diff --git a/.claude/commands/planner.md b/.claude/commands/planner.md new file mode 100644 index 0000000000..ec10090dc4 --- /dev/null +++ b/.claude/commands/planner.md @@ -0,0 +1,130 @@ +--- +description: Plan and create a repetitive task automation (prompt.md + tasks.json pair) +allowed-tools: Read, Write, Edit, Glob, Grep, Bash(mkdir:*), Bash(ls:*), AskUserQuestion +--- + +# Task Planner - Create Automated Task Workflows + +You are setting up a new **repetitive task automation** for Claude Code. The goal is to create a folder in `.ai//` containing: +- `prompt.md` - Detailed instructions for the autonomous agent +- `tasks.json` - List of tasks with completion tracking + +This pair can then be executed via `.claude/iterate.ps1 `. + +## Your Workflow + +### 1. Understand the Goal + +First, understand what the user wants to automate. Ask clarifying questions using AskUserQuestion if needed: +- What is the overall goal/feature being implemented? +- What are the individual tasks involved? +- Are there dependencies between tasks? +- What files/areas of the codebase are involved? +- Are there any reference examples or patterns to follow? + +### 2. Choose a Feature Name + +The `` should be: +- Short (1-2 words, lowercase, hyphen-separated) +- Easy to type on command line +- Descriptive of the work being done +- Not already used in `.ai/` + +Check existing folders: +```bash +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 + +Create `.ai//`: + +**prompt.md** should include: +- Overview of what we're doing +- Architecture/context needed +- Step-by-step instructions for each task type +- Code patterns and examples +- Build/test commands +- Commit message format (see below) + +### Commit Message Guidelines + +All prompts should specify commit message length requirements: +- **Soft limit**: ~50 characters (ideal length for first line) +- **Hard limit**: 76 characters (must not exceed) + +Example instruction for prompt.md: +``` +## Commit Format + +First line: Short summary ending with a dot (aim for ~50 chars, max 76 chars) + + + +IMPORTANT: Never try to commit files in .ai/ +``` + +**tasks.json** format: +```json +{ + "tasks": [ + { + "id": "task-id", + "title": "Short task title", + "description": "Detailed description of what to do", + "started": false, + "completed": false, + "dependencies": ["other-task-id"] + } + ] +} +``` + +### 5. Iterate with the User + +After creating initial files, the user may want to: +- Add more tasks to tasks.json +- Refine the prompt with more details +- Add examples or patterns +- Clarify instructions + +Keep refining until the user is satisfied. + +## Arguments + +If `$ARGUMENTS` is provided, it's the feature name to use: +- `$ARGUMENTS` = "$ARGUMENTS" + +If empty, you'll need to determine/suggest a name based on the discussion. + +## Examples + +### Example 1: Settings Migration +``` +/taskplanner settings-upgrade +``` +Creates `.ai/settings-upgrade/` with prompt and tasks for migrating settings sections. + +### Example 2: Open-ended +``` +/taskplanner +``` +Starts a conversation to understand what needs to be automated, then creates the appropriate folder. + +## Starting Point + +Let's begin! Please describe: +1. What repetitive coding task do you want to automate? +2. What is the end goal? +3. Do you have initial tasks in mind, or should we discover them together? diff --git a/.claude/commands/task.md b/.claude/commands/task.md new file mode 100644 index 0000000000..6d4b147f45 --- /dev/null +++ b/.claude/commands/task.md @@ -0,0 +1,503 @@ +--- +description: Implement a feature or fix using multi-agent workflow with fresh context at each phase +allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, AskUserQuestion, TodoWrite +--- + +# Task - Multi-Agent Implementation Workflow + +You orchestrate a multi-phase implementation workflow that uses fresh agent spawns to work within context window limits on a large codebase. + +**Arguments:** `$ARGUMENTS` = "$ARGUMENTS" + +If `$ARGUMENTS` is provided, it's the task description. If empty, ask the user what they want implemented. + +## Overview + +The workflow is organized around **projects**. Each project lives in `.ai//` and can contain multiple sequential **tasks** (labeled `a`, `b`, `c`, ... `z`). + +Project structure: +``` +.ai// + about.md # Single source of truth for the entire project + a/ # First task + context.md # Gathered codebase context for this task + plan.md # Implementation plan + review1.md # Code review documents (up to 3) + review2.md + review3.md + b/ # Follow-up task + context.md + plan.md + review1.md + c/ # Another follow-up task + ... +``` + +- `about.md` is the project-level blueprint — a single comprehensive document describing what this project does and how it works, written as if everything is already fully implemented. It contains no temporal state ("current state", "pending changes", "not yet implemented"). It is **rewritten** (not appended to) each time a new task starts, incorporating the new task's changes as if they were always part of the design. +- Each task folder (`a/`, `b/`, ...) contains self-contained files for that task. The task's `context.md` carries all task-specific information: what specifically needs to change, the delta from the current codebase, gathered file references and code patterns. Planning, implementation, and review agents only read the current task's folder. + +## Phase 0: Setup + +⚠️ **CRITICAL: Follow-up detection MUST happen FIRST, before anything else.** + +### Step 0a: Follow-up detection (MANDATORY — do this BEFORE understanding the task) + +Extract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`. + +Then run these TWO commands using the Bash tool, IN PARALLEL, right now: +1. `ls .ai/` — to see all existing project names +2. `ls .ai//about.md` — to check if this specific project exists + +**Evaluate the results:** +- If command 2 **succeeds** (the file exists): this is a **follow-up task**. The project name is `FIRST_TOKEN`. The task description is everything in `$ARGUMENTS` AFTER `FIRST_TOKEN` (strip leading whitespace). +- If command 2 **fails** (file not found): this is a **new project**. The full `$ARGUMENTS` is the task description. + +**Do NOT proceed to step 0b until you have run these commands and determined follow-up vs new.** + +### Step 0b: Project setup + +**For new projects:** +- Using the list from command 1, pick a unique short name (1-2 lowercase words, hyphen-separated) that doesn't collide with existing projects. +- Create `.ai//` and `.ai//a/`. +- Set current task letter = `a`. + +**For follow-up tasks:** +- Scan `.ai//` 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///`. + +Then proceed to Phase 1 (Context Gathering) in both cases. Follow-up tasks do NOT skip context gathering — they go through a modified version of it. + +## Phase 1: Context Gathering + +### For New Projects (task letter = `a`) + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a context-gathering agent for a large C++ codebase (Telegram Desktop). + +TASK: + +YOUR JOB: Read CLAUDE.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. +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//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//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. +``` + +After this agent completes, read both `about.md` and `a/context.md` to verify they were written properly. + +### For Follow-up Tasks (task letter = `b`, `c`, ...) + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a context-gathering agent for a follow-up task on an existing project in a large C++ codebase (Telegram Desktop). + +NEW TASK: + +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. +2. Read .ai//about.md — this is the project-level blueprint describing everything done so far. +3. Read .ai///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//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) + +Think of about.md as "the complete description of what this project does and how it works." Someone reading it should understand the full project as a finished product, without knowing it went through multiple tasks. + +### File 2: .ai///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. +``` + +After this agent completes, read both `about.md` and `/context.md` to verify they were written properly. + +## Phase 2: Planning + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a planning agent. You must create a detailed implementation plan. + +Read these files: +- .ai///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. + +Create a detailed plan in: .ai///plan.md + +The plan.md should contain: + +## Task + + +## Approach + + +## Files to Modify + + +## Files to Create + + +## 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: +1. +2. +... + +### Phase 2: (if needed) +... + +## Build Verification +- Build command to run +- Expected outcome + +## Status +- [ ] Phase 1: +- [ ] Phase 2: (if applicable) +- [ ] Build verification +- [ ] Code review +``` + +After this agent completes, read `plan.md` to verify it was written properly. + +## Phase 3: Plan Assessment + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a plan assessment agent. Review and refine an implementation plan. + +Read these files: +- .ai///context.md +- .ai///plan.md +- Then read the actual source files referenced to verify the plan makes sense. + +Use /ultrathink to 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? +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: ` 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. +``` + +After this agent completes, read `plan.md` to verify it was assessed. + +## Phase 4: Implementation + +Now read `plan.md` yourself to understand the phases. + +For each phase in the plan that is not yet marked as done, spawn an implementation agent (Task tool, subagent_type=`general-purpose`): + +``` +You are an implementation agent working on phase of an implementation plan. + +Read these files first: +- .ai///context.md - Full codebase context +- .ai///plan.md - Implementation plan + +Then read the source files you'll be modifying. + +YOUR TASK: Implement ONLY Phase 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.) +- Do NOT modify .ai/ files except to update the Status section in plan.md +- When done, update plan.md Status section: change `- [ ] Phase : ...` to `- [x] Phase : ...` +- 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, spawn the next implementation agent. +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). + +Spawn a build verification agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a build verification agent. + +Read these files: +- .ai///context.md +- .ai///plan.md + +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 +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 + 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 +- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry + +When finished, report the build result. +``` + +After the build agent returns, read `plan.md` to confirm the final status. Then proceed to Phase 6. + +## 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. Spawn review agent (Step 6a) with iteration R + 2. Read review.md verdict: + - "APPROVED" → go to FINISH + - Has improvement suggestions → spawn 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 + +Spawn an agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a code review agent for Telegram Desktop (C++ / Qt). + +Read these files: +- .ai///context.md - Codebase context +- .ai///plan.md - Implementation plan +- REVIEW.md - Style and formatting rules to enforce + 1, also read:> +- .ai///review.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. + +Use /ultrathink to 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 CLAUDE.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///review.md + +The review document should contain: + +## Code Review - Iteration + +## Summary +<1-2 sentence overall assessment> + +## Verdict: + + + + + +## Changes Required + +### +- **Category**: +- **File(s)**: +- **Problem**: +- **Fix**: + +### +... + +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. +``` + +After the review agent returns, read `review.md`. If the verdict is APPROVED, proceed to Completion. If NEEDS_CHANGES, spawn the fix agent. + +### Step 6b: Review Fix Agent + +Spawn an agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a review fix agent. You implement improvements identified during code review. + +Read these files: +- .ai///context.md - Codebase context +- .ai///plan.md - Original implementation plan +- .ai///review.md - Code review with required changes + +Then read the source files mentioned in the review. + +YOUR TASK: Implement ALL changes listed in review.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: cmake --build "c:\Telegram\tdesktop\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. +- Do NOT modify .ai/ files. + +When finished, report what changes were made. +``` + +After the fix agent returns, increment R and loop back to Step 6a (unless R > 3, in which case proceed to Completion). + +## 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. Remind the user of the project name so they can use `/task ` for follow-up changes. + +## Error Handling + +- If any agent 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 an agent, re-spawn that agent with more specific instructions. +- If build errors persist after the build agent's attempts, report the remaining errors to the user. +- If a review fix agent introduces new build errors that it cannot resolve, report to the user. diff --git a/.claude/commands/withtest.md b/.claude/commands/withtest.md new file mode 100644 index 0000000000..739c583be8 --- /dev/null +++ b/.claude/commands/withtest.md @@ -0,0 +1,656 @@ +--- +description: Implement a feature using multi-agent workflow, then iteratively test and fix it in-app +allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, AskUserQuestion, TodoWrite +--- + +# WithTest - Multi-Agent Implementation + Testing Workflow + +You orchestrate a multi-phase implementation workflow followed by an iterative testing/fixing loop. This is an extended version of `/task` that adds in-app programmatic testing after the build succeeds. + +**Arguments:** `$ARGUMENTS` = "$ARGUMENTS" + +If `$ARGUMENTS` is provided, it's the task description. If empty, ask the user what they want implemented. + +## Overview + +The workflow produces `.ai//` containing: +- `context.md` - Gathered codebase context relevant to the task +- `plan.md` - Detailed implementation plan with phases and status +- `testN.md` - Test plan for iteration N +- `resultN.md` - Test result report for iteration N +- `planN.md` - Fix plan for iteration N (if implementation bugs found) +- `screenshots/` - Screenshots captured during test runs + +Two major stages: +1. **Implementation** (Phases 0-5) - same as `/task` +2. **Testing Loop** (Phase 6) - iterative test-plan → test-do → test-run → test-check cycle + +--- + +## STAGE 1: IMPLEMENTATION (Phases 0-5) + +These phases are identical to the `/task` workflow. + +### Phase 0: Setup + +1. Understand the task from `$ARGUMENTS` or ask the user. +2. **Follow-up detection:** Check if `$ARGUMENTS` starts with a task name (the first word/token before any whitespace or newline). Look for `.ai//` directory: + - If `.ai//` exists AND contains both `context.md` and `plan.md`, this is a **follow-up task**. Read both files. The rest of `$ARGUMENTS` (after the task name) is the follow-up task description describing what additional changes are needed. + - If no matching directory exists, this is a **new task** - proceed normally. +3. For new tasks: check existing folders in `.ai/` to pick a unique short name (1-2 lowercase words, hyphen-separated) and create `.ai//`. +4. For follow-up tasks: the folder already exists, skip creation. + +### Follow-up Task Flow + +When a follow-up task is detected (existing `.ai//` with `context.md` and `plan.md`): + +1. Skip Phase 1 (Context Gathering) - context already exists. +2. Skip Phase 2 (Planning) - original plan already exists. +3. Go directly to **Phase 2F (Follow-up Planning)** instead of Phase 3. + +**Phase 2F: Follow-up Planning** + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt: + +``` +You are a planning agent for a follow-up task on an existing implementation. + +Read these files: +- .ai//context.md - Previously gathered codebase context +- .ai//plan.md - Previous implementation plan (already completed) + +Then read the source files referenced in context.md and plan.md to understand what was already implemented. + +FOLLOW-UP TASK: + +The previous plan was already implemented and tested. Now there are follow-up changes needed. + +YOUR JOB: +1. Understand what was already done from plan.md (look at the completed phases). +2. Read the actual source files to see the current state of the code. +3. If context.md needs updates for the follow-up task (new files relevant, new patterns needed), update it with additional sections marked "## Follow-up Context (iteration 2)" or similar. +4. Create a NEW follow-up plan. Update plan.md by: + - Keep the existing content as history (do NOT delete it) + - Add a new section at the end: + + --- + ## Follow-up Task + + + ## Follow-up Approach + + + ## Follow-up Files to Modify + + + ## Follow-up Implementation Steps + + ### Phase F1: + 1. + 2. ... + + ### Phase F2: (if needed) + ... + + ## Follow-up Status + Phases: + - [ ] Phase F1: + - [ ] Phase F2: (if applicable) + - [ ] Build verification + - [ ] 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. +``` + +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. + +### New Task Flow + +When this is a new task (no existing folder), proceed with Phases 1-5 as described below. + +### Phase 1: Context Gathering + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a context-gathering agent for a large C++ codebase (Telegram Desktop). + +TASK: + +YOUR JOB: Read CLAUDE.md, inspect the codebase, find ALL files and code relevant to this task, and write a comprehensive context document. + +Steps: +1. Read CLAUDE.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 your findings to: .ai//context.md + +The context.md 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. +``` + +After this agent completes, read `context.md` to verify it was written properly. + +### Phase 2: Planning + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a planning agent. You must create a detailed implementation plan. + +Read these files: +- .ai//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. + +Create a detailed plan in: .ai//plan.md + +The plan.md should contain: + +## Task + + +## Approach + + +## Files to Modify + + +## Files to Create + + +## 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: +1. +2. +... + +### Phase 2: (if needed) +... + +## Build Verification +- Build command to run +- Expected outcome + +## Status +- [ ] Phase 1: +- [ ] Phase 2: (if applicable) +- [ ] Build verification +- [ ] Testing +``` + +After this agent completes, read `plan.md` to verify it was written properly. + +### Phase 3: Plan Assessment + +Spawn an agent (Task tool, subagent_type=`general-purpose`) with this prompt structure: + +``` +You are a plan assessment agent. Review and refine an implementation plan. + +Read these files: +- .ai//context.md +- .ai//plan.md +- Then read the actual source files referenced to verify the plan makes sense. + +Use /ultrathink to 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? +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: ` 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. +``` + +After this agent completes, read `plan.md` to verify it was assessed. + +### Phase 4: Implementation + +Now read `plan.md` yourself to understand the phases. + +For each phase in the plan that is not yet marked as done, spawn an implementation agent (Task tool, subagent_type=`general-purpose`): + +``` +You are an implementation agent working on phase of an implementation plan. + +Read these files first: +- .ai//context.md - Full codebase context +- .ai//plan.md - Implementation plan + +Then read the source files you'll be modifying. + +YOUR TASK: Implement ONLY Phase 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.) +- Do NOT modify .ai/ files except to update the Status section in plan.md +- When done, update plan.md Status section: change `- [ ] Phase : ...` to `- [x] Phase : ...` +- 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, spawn the next implementation agent. +3. If all phases are done, proceed to build verification. + +### Phase 5: Build Verification + +Spawn a build verification agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a build verification agent. + +Read these files: +- .ai//context.md +- .ai//plan.md + +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 +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 + 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 +- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry + +When finished, report the build result. +``` + +After the build agent returns, read `plan.md` to confirm build verification passed. If it did, proceed to Stage 2. + +--- + +## STAGE 2: TESTING LOOP (Phase 6) + +This stage iteratively tests the implementation in-app and fixes issues. It maintains an iteration counter `N` starting at 1. + +**Key concept:** Since the project has tight coupling and no unit test infrastructure, we test by injecting `#ifdef _DEBUG` blocks into the app code that perform actions, write to `log.txt`, save screenshots, and call `Core::Quit()` when done. An agent then runs the app and observes the output. + +### Git Submodule Awareness + +Before ANY git operation (commit, stash, stash pop), the agent must: +1. Run `git submodule status` to check for modified submodules. +2. If submodules have changes, commit/stash those submodules FIRST, individually: + ``` + cd && git add -A && git commit -m "[wip-N] test changes" && cd + ``` + or for stash: + ``` + cd && git stash && cd + ``` +3. Then operate on the main repo. + +### Step 6a: Test Plan (test-plan agent) + +Spawn an agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a test-planning agent for Telegram Desktop (C++ / Qt). + +Read these files: +- .ai//context.md +- .ai//plan.md + 1, also include:> +- .ai//result.md - Previous test result + +- .ai//plan.md - Fix plan that was just implemented + +CURRENT ITERATION: + +YOUR TASKS: + +1. **Commit current implementation changes.** + - Run `git submodule status` to check for modified submodules. + - If any submodules are dirty, go into each one and commit: + `cd && git add -A && git commit -m "[wip-]" && cd ` + - Then in main repo: `git add -A && git commit -m "[wip-]"` + - Do NOT add files in .ai/ to the commit. + +2. 1> **Restore previous test code.** + - Run `git submodule status` and `git stash list` in any dirty submodules to check for stashed test code. + - Pop submodule stashes first: `cd && git stash pop && cd ` + - Then pop main repo stash: `git stash pop` + - Read the previous test.md to understand what was tested before. + - 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. + + The test code must: + - Be wrapped in `#ifdef _DEBUG` blocks so it only runs in Debug builds + - Be injected at appropriate points in the app lifecycle (e.g., after main window shows, after chats load, etc.) + - Write progress and results to a log file. Use a dedicated path like: + `QFile logFile("c:/Telegram/tdesktop/.ai//test_log.txt");` + Open with `QIODevice::Append | QIODevice::Text`, write with QTextStream, and flush after every write. + - Save screenshots where visual verification is needed: + `widget->grab().save("c:/Telegram/tdesktop/.ai//screenshots/.png");` + Log each screenshot save: `"SCREENSHOT: "` + - Use `QTimer::singleShot(...)` or deferred calls to schedule test steps after UI events settle + - Call `Core::Quit()` when all test steps complete, so the app exits cleanly + - Log `"TEST_COMPLETE"` right before `Core::Quit()` so the test-run agent knows testing finished + - Log `"TEST_STEP: "` before each major step for progress tracking + - Log `"TEST_RESULT: PASS: "` or `"TEST_RESULT: FAIL: -
"` for each check + + Consider what needs testing: + - Does the new UI appear correctly? + - Do interactions work (clicks, navigation)? + - Does data flow correctly? + - Are there edge cases to verify? + +4. **Write the test plan** to `.ai//test.md` containing: + + ## Test Iteration + ## What We're Testing + + + ## Test Steps + 1. : what we do, what we expect, how we verify + 2. ... + + ## Code Injection Points + - File: , Location: , Purpose: + - ... + + ## Expected Log Output + + + ## Expected Screenshots + - .png: should show + - ... + + ## Success Criteria + - + - + - ... + +When finished, report what test plan was created. +``` + +### Step 6b: Test Implementation (test-do agent) + +Spawn an agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a test implementation agent for Telegram Desktop (C++ / Qt). + +Read these files: +- .ai//context.md +- .ai//plan.md +- .ai//test.md - The test plan to implement + +YOUR TASK: Implement the test code described in test.md. + +Rules: +- ALL test code MUST be inside `#ifdef _DEBUG` blocks +- Place test code at the injection points specified in the test plan +- Make sure the screenshots folder exists: create `.ai//screenshots/` directory +- Delete any old test_log.txt before the test starts (in code, at the first test step) +- Use QTimer::singleShot for delayed operations to let the UI settle +- Flush log writes immediately (don't buffer) +- End with logging "TEST_COMPLETE" and calling Core::Quit() +- Follow CLAUDE.md coding conventions +- Make sure the code compiles: run `cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram` +- If build fails, fix errors and rebuild until it passes +- If build fails with file-locked errors (C1041, LNK1104), STOP and report + +When finished, report what test code was added and where. +``` + +### Step 6c: Test Run (test-run agent) + +Spawn an agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a test execution agent. You run the Telegram Desktop app and observe test output. + +Read these files: +- .ai//test.md - The test plan (so you know what to expect) + +YOUR TASK: Run the built app and monitor test execution. + +Steps: + +1. **Prepare.** + - Delete old test_log.txt if it exists: `del "c:\Telegram\tdesktop\docs\ai\work\\test_log.txt" 2>nul` + - Ensure screenshots folder exists: `mkdir "c:\Telegram\tdesktop\docs\ai\work\\screenshots" 2>nul` + +2. **Launch the app.** + - Run in background: `start "" "c:\Telegram\tdesktop\out\Debug\Telegram.exe"` + - Note the time of launch. + +3. **Monitor test_log.txt in a polling loop.** + - Every 5 seconds, read the log file to check for new output. + - When you see `"SCREENSHOT: "`, read the screenshot image file to visually verify it. + - Track which TEST_STEP entries appear. + - Track TEST_RESULT entries (PASS/FAIL). + +4. **Detect completion or failure.** + - **Success**: Log contains `"TEST_COMPLETE"` - the app should exit on its own shortly after. + - **Crash**: The process disappears before `"TEST_COMPLETE"`. Check for crash dumps or error dialogs. + - **Hang/Timeout**: If no new log output for 120 seconds and no `"TEST_COMPLETE"`, kill the process: + `taskkill /IM Telegram.exe /F` + - **No log at all**: If no test_log.txt appears within 60 seconds of launch, kill the process. + +5. **After the process exits (or is killed), wait 5 seconds, then:** + - Read the full final test_log.txt + - Read all screenshot files saved during the test + - Check for any leftover Telegram.exe processes: `tasklist /FI "IMAGENAME eq Telegram.exe"` and kill if needed + +6. **Write the result report** to `.ai//result.md`: + + ## Test Result - Iteration + ## Outcome: + + ## Log Output + + + ## Screenshot Analysis + - .png: .md> + - ... + + ## Test Results Summary + - PASS: + - FAIL: + + ## Issues Found + + + ## Raw Details + + +When finished, report the test outcome. +``` + +After the test-run agent returns, read `result.md`. + +### Step 6d: Test Assessment (test-check agent) + +Spawn an agent (Task tool, subagent_type=`general-purpose`): + +``` +You are a test assessment agent. You analyze test results and decide next steps. + +Read these files: +- .ai//context.md +- .ai//plan.md +- .ai//test.md +- .ai//result.md + 1, also read previous test/result pairs for history> + +Use /ultrathink to carefully analyze the test results. + +DECIDE one of three outcomes: + +### Outcome A: ALL TESTS PASS +If all test results are PASS and screenshots look correct: +1. Write to result.md (append): `\n## Verdict: PASS` +2. Report "ALL_TESTS_PASS" so the orchestrator knows to finish. + +### Outcome B: TEST CODE NEEDS CHANGES +If the test itself was flawed (wrong assertions, bad timing, insufficient waits, screenshot taken too early, wrong injection point, etc.) but the implementation seems correct: +1. Describe what's wrong with the test and what to change. +2. Make the changes directly to the test code in the source files. +3. Rebuild: `cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram` +4. If build fails with file-locked errors (C1041, LNK1104), STOP and report. +5. Write the updated test description to `.ai//test.md` explaining what changed and why. +6. Report "TEST_NEEDS_RERUN" so the orchestrator goes back to step 6c. + +### Outcome C: IMPLEMENTATION HAS BUGS +If the test results indicate actual bugs in the implementation (not test issues): +1. Analyze what's wrong with the implementation. +2. Write a fix plan to `.ai//plan.md`: + + ## Fix Plan - Iteration + ## Problem + + + ## Root Cause + + + ## Fix Steps + 1. + 2. ... + +3. Stash the test code (it will be restored later): + - Run `git submodule status` and stash dirty submodules first: + `cd && git stash && cd ` + - Then: `git stash` +4. Report "IMPLEMENTATION_NEEDS_FIX" so the orchestrator goes to re-implementation. + +When finished, report your verdict clearly as one of: ALL_TESTS_PASS, TEST_NEEDS_RERUN, IMPLEMENTATION_NEEDS_FIX. +``` + +### Orchestrator Loop Logic + +After Phase 5 (build verification) succeeds, you (the orchestrator) run the testing loop: + +``` +Set N = 1 + +LOOP: + 1. Spawn test-plan agent (Step 6a) with iteration N + 2. Spawn test-do agent (Step 6b) with iteration N + 3. Spawn test-run agent (Step 6c) with iteration N + 4. Spawn test-check agent (Step 6d) with iteration N + 5. Read the verdict: + - "ALL_TESTS_PASS" → go to FINISH + - "TEST_NEEDS_RERUN" → + N = N + 1 + go to step 3 (skip 6a and 6b, test code was already updated by test-check) + - "IMPLEMENTATION_NEEDS_FIX" → + Spawn implementation fix agent (see below) + N = N + 1 + go to step 1 (full restart: new commit, stash pop test code, etc.) + 6. Safety: if N > 5, stop and report to user - too many iterations. + +FINISH: + - Stash or revert all test code (#ifdef _DEBUG blocks): + - git submodule status, stash submodules if dirty + - git stash (to save test code separately, user may want it later) + - Update plan.md: change `- [ ] Testing` to `- [x] Testing` + - Report to user +``` + +### Implementation Fix Agent + +When test-check reports IMPLEMENTATION_NEEDS_FIX, spawn this agent: + +``` +You are an implementation fix agent. + +Read these files: +- .ai//context.md +- .ai//plan.md +- .ai//plan.md - The fix plan from test assessment + +Then read the source files mentioned in the fix plan. + +YOUR TASK: Implement the fixes described in plan.md. + +Steps: +1. Read and understand the fix plan. +2. Make the specified code changes. +3. Build: `cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram` +4. Fix any build errors. +5. If build fails with file-locked errors (C1041, LNK1104), STOP and report. + +Rules: +- Only make changes specified in the fix plan +- Follow CLAUDE.md conventions +- Do NOT touch test code or .ai/ files (except plan.md status if relevant) + +When finished, report what was fixed. +``` + +--- + +## Completion + +When the testing loop finishes (ALL_TESTS_PASS or user stops it): +1. Read the final `plan.md` and report full summary to the user. +2. List all files modified/created by the implementation. +3. Summarize test iterations: how many rounds, what was found and fixed. +4. Note that test code is stashed (available via `git stash pop` if needed). +5. Note any remaining concerns. + +## Error Handling + +- If any agent 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 an agent, re-spawn that agent with more specific instructions. +- If build errors persist after agent attempts, report remaining errors to the user. +- If the testing loop exceeds 5 iterations, stop and report - something fundamental may be wrong. +- If the app crashes repeatedly, report to user - may need manual investigation. +- If file-locked build errors occur at ANY point, stop immediately and ask user to close Telegram.exe. diff --git a/.claude/iterate.ps1 b/.claude/iterate.ps1 new file mode 100644 index 0000000000..be038920b7 --- /dev/null +++ b/.claude/iterate.ps1 @@ -0,0 +1,343 @@ +#!/usr/bin/env pwsh +# Iterative Task Runner +# Runs Claude Code in a loop to complete tasks from a taskplanner-created folder +# +# Usage: .\docs\ai\iterate.ps1 [-MaxIterations N] [-Interactive] [-DryRun] [-SingleCommit] [-NoCommit] +# +# Arguments: +# featurename Name of the folder in .ai/ containing prompt.md and tasks.json +# -MaxIterations Maximum iterations before stopping (default: 50) +# -Interactive Pause between iterations for user confirmation (default: auto/no pause) +# -DryRun Show what would be executed without running +# -SingleCommit Don't commit after each task, commit all changes at the end +# -NoCommit Don't commit at all (no per-task commits, no final commit) + +param( + [Parameter(Position=0, Mandatory=$true)] + [string]$FeatureName, + + [int]$MaxIterations = 50, + [switch]$Interactive, + [switch]$DryRun, + [switch]$SingleCommit, + [switch]$NoCommit +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Resolve-Path (Join-Path $ScriptDir "..\..") +$WorkDir = Join-Path $ScriptDir "work\$FeatureName" +$PromptMd = Join-Path $WorkDir "prompt.md" +$TasksJson = Join-Path $WorkDir "tasks.json" + +$BuildOutputDir = Join-Path $RepoRoot "out\Debug" +$TelegramExe = Join-Path $BuildOutputDir "Telegram.exe" +$TelegramPdb = Join-Path $BuildOutputDir "Telegram.pdb" + +function Format-Duration { + param([int]$Seconds) + + if ($Seconds -lt 60) { + return "${Seconds}s" + } elseif ($Seconds -lt 3600) { + $min = [math]::Floor($Seconds / 60) + $sec = $Seconds % 60 + return "${min}m ${sec}s" + } else { + $hr = [math]::Floor($Seconds / 3600) + $min = [math]::Floor(($Seconds % 3600) / 60) + $sec = $Seconds % 60 + return "${hr}h ${min}m ${sec}s" + } +} + +function Test-BuildFilesUnlocked { + $filesToCheck = @($TelegramExe, $TelegramPdb) + + foreach ($file in $filesToCheck) { + if (Test-Path $file) { + try { + Remove-Item $file -Force -ErrorAction Stop + Write-Host "Removed: $file" -ForegroundColor DarkGray + } + catch { + Write-Host "" + Write-Host "========================================" -ForegroundColor Red + Write-Host " ERROR: Cannot delete build output" -ForegroundColor Red + Write-Host " File is locked: $file" -ForegroundColor Red + Write-Host "" -ForegroundColor Red + Write-Host " Please close Telegram.exe and any" -ForegroundColor Red + Write-Host " debugger, then try again." -ForegroundColor Red + Write-Host "========================================" -ForegroundColor Red + Write-Host "" + return $false + } + } + } + return $true +} + +function Show-ClaudeStream { + param([string]$Line) + + try { + $obj = $Line | ConvertFrom-Json -ErrorAction Stop + + switch ($obj.type) { + "assistant" { + if ($obj.message.content) { + foreach ($block in $obj.message.content) { + if ($block.type -eq "text") { + Write-Host $block.text -ForegroundColor White + } + elseif ($block.type -eq "tool_use") { + $summary = "" + if ($block.input) { + if ($block.input.file_path) { + $summary = $block.input.file_path + } elseif ($block.input.pattern) { + $summary = $block.input.pattern + } elseif ($block.input.command) { + $cmd = $block.input.command + if ($cmd.Length -gt 60) { $cmd = $cmd.Substring(0, 60) + "..." } + $summary = $cmd + } else { + $inputStr = $block.input | ConvertTo-Json -Compress -Depth 1 + if ($inputStr.Length -gt 60) { $inputStr = $inputStr.Substring(0, 60) + "..." } + $summary = $inputStr + } + } + Write-Host "[Tool: $($block.name)] $summary" -ForegroundColor Yellow + } + } + } + } + "user" { + # Tool results - skip verbose output + } + "result" { + Write-Host "`n--- Session Complete ---" -ForegroundColor Cyan + if ($obj.cost_usd) { + Write-Host "Cost: `$$($obj.cost_usd)" -ForegroundColor DarkCyan + } + } + "system" { + # System messages - skip + } + } + } + catch { + # Not valid JSON, skip + } +} + +# Verify feature folder exists +if (-not (Test-Path $WorkDir)) { + Write-Error "Feature folder not found: $WorkDir`nRun '/taskplanner $FeatureName' first to create it." + exit 1 +} + +# Verify required files exist +foreach ($file in @($PromptMd, $TasksJson)) { + if (-not (Test-Path $file)) { + Write-Error "Required file not found: $file" + exit 1 + } +} + +if ($SingleCommit -or $NoCommit) { + $AfterImplementation = @" + - Mark the task completed in tasks.json ("completed": true) + - If new tasks emerged, add them to tasks.json +"@ + $CommitRule = "- Do NOT commit changes after task is done, just mark it as done in tasks.json. Commit will be done when all tasks are complete, separately." +} else { + $AfterImplementation = @" + - Mark the task completed in tasks.json ("completed": true) + - Commit your changes + - If new tasks emerged, add them to tasks.json +"@ + $CommitRule = "" +} + +$Prompt = @" +You are an autonomous coding agent working on: $FeatureName + +Read these files for context: +- .ai/$FeatureName/prompt.md - Detailed instructions and architecture +- .ai/$FeatureName/tasks.json - Task list with completion status + +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 +3. Implement that ONE task only +4. After successful implementation: +$AfterImplementation + +## Critical Rules + +- Only mark a task complete if you verified the work is done (build passes, etc.) +- If stuck, document the issue in the task's notes field and move on +- Do ONE task per iteration, then stop +- NEVER try to commit files in .ai/ +$CommitRule + +## Completion Signal + +If ALL tasks in tasks.json have "completed": true, output exactly: +===ALL_TASKS_COMPLETE=== +"@ + +$CommitPrompt = @" +You are an autonomous coding agent. All tasks for "$FeatureName" are now complete. + +Your job: Create a single commit with all the changes. + +## Steps + +1. Run git status to see all modified files +2. Run git diff to review the changes +3. Create a commit with a short summary (aim for ~50 chars, max 76 chars) describing what was implemented +4. The commit message should describe the overall feature/fix, not list individual changes + +## Critical Rules + +- NEVER try to commit files in .ai/ +- Use a concise commit message that captures the essence of the work done +"@ + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Iterative Task Runner" -ForegroundColor Cyan +Write-Host " Feature: $FeatureName" -ForegroundColor Cyan +Write-Host " Max iterations: $MaxIterations" -ForegroundColor Cyan +Write-Host " Mode: $(if ($Interactive) { 'Interactive' } else { 'Auto' })" -ForegroundColor Cyan +Write-Host " Commit: $(if ($NoCommit) { 'None' } elseif ($SingleCommit) { 'Single (at end)' } else { 'Per task' })" -ForegroundColor Cyan +Write-Host " Working directory: $RepoRoot" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +if ($DryRun) { + Write-Host "[DRY RUN] Would execute with prompt:" -ForegroundColor Yellow + Write-Host $Prompt + Write-Host "" + Write-Host "Feature folder: $WorkDir" -ForegroundColor Yellow + Write-Host "Prompt file: $PromptMd" -ForegroundColor Yellow + Write-Host "Tasks file: $TasksJson" -ForegroundColor Yellow + exit 0 +} + +Push-Location $RepoRoot + +$ScriptStartTime = Get-Date +$IterationTimes = @() + +try { + for ($i = 1; $i -le $MaxIterations; $i++) { + Write-Host "" + Write-Host "========================================" -ForegroundColor Yellow + Write-Host " Iteration $i of $MaxIterations" -ForegroundColor Yellow + Write-Host "========================================" -ForegroundColor Yellow + Write-Host "" + + if (-not (Test-BuildFilesUnlocked)) { + exit 1 + } + + $IterationStartTime = Get-Date + + claude --dangerously-skip-permissions --verbose -p $Prompt --output-format stream-json 2>&1 | ForEach-Object { + Show-ClaudeStream $_ + } + + $IterationEndTime = Get-Date + $IterationDuration = [int]($IterationEndTime - $IterationStartTime).TotalSeconds + $IterationTimes += $IterationDuration + Write-Host "Iteration time: $(Format-Duration $IterationDuration)" -ForegroundColor DarkCyan + + # Check task status after each run + $tasks = Get-Content $TasksJson | ConvertFrom-Json + $incomplete = @($tasks.tasks | Where-Object { -not $_.completed }) + $inProgress = @($tasks.tasks | Where-Object { $_.started -and -not $_.completed }) + + if ($incomplete.Count -eq 0) { + if ($SingleCommit -and -not $NoCommit) { + $i++ + if ($i -le $MaxIterations) { + Write-Host "" + Write-Host "========================================" -ForegroundColor Yellow + Write-Host " Final commit iteration" -ForegroundColor Yellow + Write-Host "========================================" -ForegroundColor Yellow + Write-Host "" + + $CommitStartTime = Get-Date + + claude --dangerously-skip-permissions --verbose -p $CommitPrompt --output-format stream-json 2>&1 | ForEach-Object { + Show-ClaudeStream $_ + } + + $CommitEndTime = Get-Date + $CommitDuration = [int]($CommitEndTime - $CommitStartTime).TotalSeconds + $IterationTimes += $CommitDuration + Write-Host "Commit time: $(Format-Duration $CommitDuration)" -ForegroundColor DarkCyan + } else { + Write-Host "" + Write-Host "========================================" -ForegroundColor Red + Write-Host " Max iterations reached before commit" -ForegroundColor Red + Write-Host " Run manually: git add . && git commit" -ForegroundColor Red + Write-Host "========================================" -ForegroundColor Red + Write-Host "" + exit 1 + } + } + + $TotalTime = [int]((Get-Date) - $ScriptStartTime).TotalSeconds + $AvgTime = if ($IterationTimes.Count -gt 0) { [int](($IterationTimes | Measure-Object -Sum).Sum / $IterationTimes.Count) } else { 0 } + + Write-Host "" + Write-Host "========================================" -ForegroundColor Green + Write-Host " ALL TASKS COMPLETE!" -ForegroundColor Green + Write-Host " Feature: $FeatureName" -ForegroundColor Green + Write-Host " Iterations: $($IterationTimes.Count)" -ForegroundColor Green + Write-Host " Total time: $(Format-Duration $TotalTime)" -ForegroundColor Green + Write-Host " Avg per iteration: $(Format-Duration $AvgTime)" -ForegroundColor Green + Write-Host "========================================" -ForegroundColor Green + Write-Host "" + + exit 0 + } + + Write-Host "" + Write-Host "Remaining tasks: $($incomplete.Count)" -ForegroundColor Cyan + if ($inProgress.Count -gt 0) { + Write-Host "In progress: $($inProgress[0].title)" -ForegroundColor Yellow + } + + if ($Interactive) { + Write-Host "Press Enter to continue, Ctrl+C to stop..." -ForegroundColor Cyan + Read-Host + } else { + Start-Sleep -Seconds 2 + } + } + + $TotalTime = [int]((Get-Date) - $ScriptStartTime).TotalSeconds + $AvgTime = if ($IterationTimes.Count -gt 0) { [int](($IterationTimes | Measure-Object -Sum).Sum / $IterationTimes.Count) } else { 0 } + + Write-Host "" + Write-Host "========================================" -ForegroundColor Red + Write-Host " Max iterations ($MaxIterations) reached" -ForegroundColor Red + Write-Host " Check tasks.json for remaining tasks" -ForegroundColor Red + Write-Host " Total time: $(Format-Duration $TotalTime)" -ForegroundColor Red + Write-Host " Avg per iteration: $(Format-Duration $AvgTime)" -ForegroundColor Red + Write-Host "========================================" -ForegroundColor Red + Write-Host "" + exit 1 +} +finally { + Pop-Location +} diff --git a/.codex/skills/task-think/PROMPTS.md b/.codex/skills/task-think/PROMPTS.md new file mode 100644 index 0000000000..f86d962849 --- /dev/null +++ b/.codex/skills/task-think/PROMPTS.md @@ -0,0 +1,227 @@ +# Phase Prompts + +Use these templates for `codex exec --json` child runs. Replace ``, ``, ``, and ``. + +## 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//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//` and `.ai//a/` and `logs/`. +- Set `` = `a`. + +**For follow-up tasks:** +- Scan `.ai//` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter). +- The new task letter = next letter in sequence. +- Create `.ai///` 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 "" in repository . + +Read CLAUDE.md for the basic coding rules and guidelines. + +Read AGENTS.md and all relevant source files. Write TWO documents: + +### File 1: .ai//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//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 . + +NEW TASK: + +Read CLAUDE.md for the basic coding rules and guidelines. + +Steps: +1. Read AGENTS.md for project conventions. +2. Read .ai//about.md — the project-level blueprint describing everything done so far. +3. Read .ai///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//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///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 "" in repository . + +Read CLAUDE.md for the basic coding rules and guidelines. + +Read: +- .ai///context.md + +Then read the specific source files referenced in context.md to understand the code deeply. + +Create: +- .ai///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 "" in repository . + +Read CLAUDE.md for the basic coding rules and guidelines. + +Read: +- .ai///context.md +- .ai///plan.md + +Implement the plan in code. Then write: +- .ai///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 "" in repository . + +Read CLAUDE.md for the basic coding rules and guidelines. + +Read: +- .ai///plan.md +- .ai///implementation.md + +Run the relevant build/test commands from AGENTS.md and plan.md. +Append results to: +- .ai///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 "" in repository . + +Read AGENTS.md for the basic coding rules and guidelines. +Read REVIEW.md for the style and formatting rules you must enforce. + +Read: +- .ai///context.md +- .ai///plan.md +- .ai///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///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 "" | Tee-Object .ai///logs/phase-1-context.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-2-plan.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-3-implement.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-4-verify.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-5-review.jsonl +``` diff --git a/.codex/skills/task-think/SKILL.md b/.codex/skills/task-think/SKILL.md new file mode 100644 index 0000000000..19f1260a33 --- /dev/null +++ b/.codex/skills/task-think/SKILL.md @@ -0,0 +1,95 @@ +--- +name: task-think +description: Orchestrate a multi-phase implementation workflow for this repository with artifact files under .ai/// 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. +--- + +# Task Pipeline + +Run a full implementation workflow with repository artifacts and clear phase boundaries. + +## Inputs + +Collect: +- task description +- optional project name (if missing, derive a short kebab-case name) +- optional constraints (files, architecture, deadlines, risk tolerance) +- optional screenshot paths + +If screenshots are attached in UI but not present as files, write a brief textual summary in `.ai//about.md` so child runs can consume the requirements. + +## Overview + +The workflow is organized around **projects**. Each project lives in `.ai//` and can contain multiple sequential **tasks** (labeled `a`, `b`, `c`, ... `z`). + +Project structure: +``` +.ai// + about.md # Single source of truth for the entire project + a/ # First task + context.md # Gathered codebase context for this task + plan.md # Implementation plan + review.md # Code review document + b/ # Follow-up task + context.md + plan.md + review.md + c/ # Another follow-up task + ... +``` + +- `about.md` is the project-level blueprint — a single comprehensive document describing what this project does and how it works, written as if everything is already fully implemented. It contains no temporal state ("current state", "pending changes", "not yet implemented"). It is **rewritten** (not appended to) each time a new task starts, incorporating the new task's changes as if they were always part of the design. +- Each task folder (`a/`, `b/`, ...) contains self-contained files for that task. The task's `context.md` carries all task-specific information: what specifically needs to change, the delta from the current codebase, gathered file references and code patterns. Planning, implementation, and review agents only read the current task's folder. + +## Artifacts + +Create and maintain: +- `.ai//about.md` +- `.ai///context.md` +- `.ai///plan.md` +- `.ai///implementation.md` +- `.ai///review.md` +- `.ai///logs/phase-*.jsonl` (when running child `codex exec`) + +## Execution Mode + +Run `codex exec --json` child sessions for each phase. + +## 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//` and `.ai//a/`. +3. For follow-up tasks: find latest task letter, create `.ai///`. +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. + +Use the phase prompt templates 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. +- Never claim completion without: + - implemented code changes present + - build/test 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. + +## User Invocation + +Use plain language with the skill name in the request, for example: + +`Use local task-think skill: make sure FileLoadTask::process does not create or read QPixmap on background threads; use QImage with ARGB32_Premultiplied instead.` + +For follow-up tasks on an existing project: + +`Use local task-think skill: my-project also handle the case where the file is already cached` + +If screenshots are relevant, include file paths in the same prompt. diff --git a/.gitignore b/.gitignore index 0ae30de9fd..6f9ce84aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -65,5 +65,8 @@ settings.local.json .cursor/* !.cursor/rules/ +# AI work folder (session-specific, not for version control) +.ai + # Project specific Telegram/SourceFiles/_other/packer_private.h diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..78c64f84a7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,424 @@ +# Agent Guide for Telegram Desktop + +This guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase. + +## Build System Structure + +The build system expects this directory layout: + +```text +L:\Telegram\ # BuildPath +L:\Telegram\tdesktop\ # Repository (you work here) +L:\Telegram\Libraries\ # 32-bit dependencies (Linux/macOS) +L:\Telegram\win64\Libraries\ # 64-bit dependencies (Windows) +L:\Telegram\ThirdParty\ # Build tools (NuGet, Python, etc.) +``` + +Dependencies are located relative to the repository: `../Libraries`, `../win64/Libraries`, or `../ThirdParty`. + +## Build Configuration + +### Build Commands + +**From repository root, run:** + +```bash +cmake --build out --config Debug --target Telegram +``` + +That's it. The `out/` directory is already configured. The executable will be at `out/Debug/Telegram.exe`. + +**Important:** When running cmake from a shell that doesn't support `cd`, use quoted absolute paths: +```bash +cmake --build "l:\Telegram\tx64\out" --config Debug --target Telegram +``` + +**Never build Release** - it's extremely heavy and not needed for testing changes. + +## Platform-Specific Requirements + +### Windows +- Requires Visual Studio 2022 +- Must run from appropriate Native Tools Command Prompt: + - "x64 Native Tools Command Prompt" for `win64` + - "x86 Native Tools Command Prompt" for `win` + - "ARM64 Native Tools Command Prompt" for `winarm` +- Dependencies: `../win64/Libraries` (64-bit) or `../Libraries` (32-bit) + +### macOS +- Requires Xcode +- Dependencies: `../Libraries/local/Qt-*` +- Set `QT` environment variable: `export QT=6.8` + +### Linux +- Build dependencies in `../Libraries` +- Set `QT` environment variable if needed + +## Key Files + +- **`Telegram/build/version`** - Version information +- **`out/`** - Build output directory + +## Troubleshooting + +### "Libraries not found" +Ensure the repository is in `L:\Telegram\tdesktop`. The build system requires `../win64/Libraries` to exist. + +### Build fails with "wrong command prompt" +On Windows, use the correct Visual Studio Native Tools Command Prompt matching your target (x64/x86/ARM64). + +### Build fails with PDB or EXE access errors + +**⚠️ CRITICAL: DO NOT RETRY THE BUILD. STOP AND WAIT FOR USER.** + +If the build fails with ANY of these errors: +- `fatal error C1041: cannot open program database` +- `cannot open output file 'Telegram.exe'` +- `LNK1104: cannot open file` +- Any "access denied" or "file in use" error + +**STOP IMMEDIATELY.** These errors mean files are locked by a running process (Telegram.exe or debugger). + +**What to do:** +1. Do NOT attempt another build - it will fail the same way +2. Do NOT try to delete files - they are locked +3. Do NOT try any workarounds or fixes +4. IMMEDIATELY inform the user: + +> "Build failed - files are locked. Please close Telegram.exe (and any debugger) so I can rebuild." + +**Then WAIT for user confirmation before attempting any build.** + +Retrying builds wastes time and context. The ONLY fix is for the user to close the running process. + +## Best Practices + +1. **Always use Debug builds** - Release builds are extremely heavy +2. **Don't build Release configuration** - it's too heavy for testing + +--- + +# Development Guidelines + +## Coding Style + +**Do NOT write comments in code:** + +This is important! Do not write single-line comments that describe what the next line does - they are bloat. Comments are allowed ONLY to describe complex algorithms in detail, when the explanation requires at least 4-5 lines. Self-documenting code with clear variable and function names is preferred. + +```cpp +// BAD - don't do this: +// Get the user's name +auto name = user->name(); +// Check if premium +if (user->isPremium()) { + +// GOOD - no comments needed, code is self-explanatory: +auto name = user->name(); +if (user->isPremium()) { + +// ACCEPTABLE - complex algorithm explanation (4+ lines): +// The algorithm works by first collecting all visible messages +// in the viewport, then calculating their intersection with +// the clip rectangle. Messages are grouped by date headers, +// and we need to account for sticky headers that may overlap +// with the first message in each group. +``` + +**Style and formatting rules** are in `REVIEW.md` — see that file for empty-line-before-closing-brace, operator placement in multi-line expressions, if-with-initializer, and other mechanical style rules. + +**Use `auto` for type deduction:** + +Prefer `auto` (or `const auto`, `const auto &`) instead of explicit types: + +```cpp +// Prefer this: +auto currentTitle = tr::lng_settings_title(tr::now); +auto nameProducer = GetNameProducer(); + +// Instead of this: +QString currentTitle = tr::lng_settings_title(tr::now); +rpl::producer nameProducer = GetNameProducer(); +``` + +## API Usage + +### API Schema Files + +API definitions use [TL Language](https://core.telegram.org/mtproto/TL): + +1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`** - MTProto protocol (encryption, auth, etc.) +2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`** - Telegram API (messages, users, chats, etc.) + +### Making API Requests + +Standard pattern using `api()`, generated `MTP...` types, and callbacks: + +```cpp +api().request(MTPnamespace_MethodName( + MTP_flags(flags_value), + MTP_inputPeer(peer), + MTP_string(messageText), + MTP_long(randomId), + MTP_vector() +)).done([=](const MTPResponseType &result) { + // Handle successful response + + // Multiple constructors - use .match() or check type: + result.match([&](const MTPDuser &data) { + // use data.vfirst_name().v + }, [&](const MTPDuserEmpty &data) { + // handle empty user + }); + + // Single constructor - use .data() shortcut: + const auto &data = result.data(); + // use data.vmessages().v + +}).fail([=](const MTP::Error &error) { + // Handle API error + if (error.type() == u"FLOOD_WAIT_X"_q) { + // Handle flood wait + } +}).handleFloodErrors().send(); +``` + +**Key points:** +- Always refer to `api.tl` for method signatures and return types +- Use generated `MTP...` types for parameters (`MTP_int`, `MTP_string`, etc.) +- For multiple constructors, use `.match()` or check `.type()` against `mtpc_` constants then call `.c_constructorName()`: + ```cpp + // Using match: + result.match([&](const MTPDuser &data) { ... }, [&](const MTPDuserEmpty &data) { ... }); + // Or explicit type check: + if (result.type() == mtpc_user) { + const auto &data = result.c_user(); // asserts on type mismatch + } + ``` +- For single constructors, use `.data()` shortcut +- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling + +## UI Styling + +### Style Files + +UI styles are defined in `.style` files using custom syntax: + +```style +using "ui/basic.style"; +using "ui/widgets/widgets.style"; + +MyButtonStyle { + textPadding: margins; + icon: icon; + height: pixels; +} + +defaultButton: MyButtonStyle { + textPadding: margins(10px, 15px, 10px, 15px); + icon: icon{{ "gui/icons/search", iconColor }}; + height: 30px; +} + +primaryButton: MyButtonStyle(defaultButton) { + icon: icon{{ "gui/icons/check", iconColor }}; +} +``` + +**Built-in types:** +- `int` - Integer numbers (e.g., `maxLines: 3;`) +- `bool` - Boolean values (e.g., `useShadow: true;`) +- `pixels` - Pixel values with `px` suffix (e.g., `10px`) +- `color` - Named colors from `ui/colors.palette` +- `icon` - Inline icon definition: `icon{{ "path/stem", color }}` +- `margins` - Four values: `margins(top, right, bottom, left)` +- `size` - Two values: `size(width, height)` +- `point` - Two values: `point(x, y)` +- `align` - Alignment: `align(center)`, `align(left)` +- `font` - Font: `font(14px semibold)` +- `double` - Floating point + +**Multi-part icons** (layers drawn bottom-up): +```style +myComplexIcon: icon{ + { "gui/icons/background", iconBgColor }, + { "gui/icons/foreground", iconFgColor } +}; +``` + +**Borders** are typically separate fields, not a single property: +```style +chatInput { + border: 1px; // width + borderFg: defaultInputFieldBorder; // color +} +``` + +**Never hardcode sizes in code:** + +The app supports different interface scale options. Style `px` values are automatically scaled at runtime, but raw integer constants in code are not. Never use hardcoded numbers for margins, paddings, spacing, sizes, coordinates, or any other dimensional values. Always define them in `.style` files and reference via `st::`. + +```cpp +// BAD - breaks at non-100% interface scale: +p.drawText(10, 20, text); +widget->setFixedHeight(48); +auto margin = 8; +auto iconSize = QSize(24, 24); + +// GOOD - define in .style file and reference: +p.drawText(st::myWidgetTextLeft, st::myWidgetTextTop, text); +widget->setFixedHeight(st::myWidgetHeight); +auto margin = st::myWidgetMargin; +auto iconSize = st::myWidgetIconSize; +``` + +### Usage in Code + +```cpp +#include "styles/style_widgets.h" + +// Access style members +int height = st::primaryButton.height; +const style::icon &icon = st::primaryButton.icon; +style::margins padding = st::primaryButton.textPadding; + +// Use in painting +void MyWidget::paintEvent(QPaintEvent *e) { + Painter p(this); + p.fillRect(rect(), st::chatInput.backgroundColor); +} +``` + +## Localization + +### String Definitions + +Strings are defined in `Telegram/Resources/langs/lang.strings`: + +``` +"lng_settings_title" = "Settings"; +"lng_confirm_delete_item" = "Are you sure you want to delete {item_name}?"; +"lng_files_selected#one" = "{count} file selected"; +"lng_files_selected#other" = "{count} files selected"; +``` + +### Usage in Code + +**Immediate (current value):** + +```cpp +auto currentTitle = tr::lng_settings_title(tr::now); + +auto currentConfirmation = tr::lng_confirm_delete_item( + tr::now, + lt_item_name, currentItemName); + +auto filesText = tr::lng_files_selected(tr::now, lt_count, count); +``` + +**Reactive (rpl::producer):** + +```cpp +auto titleProducer = tr::lng_settings_title(); + +auto confirmationProducer = tr::lng_confirm_delete_item( + lt_item_name, + std::move(itemNameProducer)); + +auto filesTextProducer = tr::lng_files_selected( + lt_count, + countProducer | tr::to_count()); +``` + +**Key points:** +- Pass `tr::now` as first argument for immediate `QString` +- Omit `tr::now` for reactive `rpl::producer` +- Placeholders use `lt_tag_name, value` pattern +- For `{count}`: immediate uses `int`, reactive uses `rpl::producer` 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: + - `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 + auto title = tr::lng_export_progress_title(tr::now, tr::bold); + auto text = tr::lng_proxy_incorrect_secret(tr::now, tr::rich); + auto linked = tr::lng_settings_birthday_contacts( + lt_link, + tr::lng_settings_birthday_contacts_link(tr::url(link)), + tr::marked); + ``` + +## RPL (Reactive Programming Library) + +### Core Concepts + +**Producers** represent streams of values over time: + +```cpp +auto intProducer = rpl::single(123); // Emits single value +auto lifetime = rpl::lifetime(); // Manages subscription lifetime +``` + +### Starting Pipelines + +```cpp +std::move(counter) | rpl::on_next([=](int value) { + qDebug() << "Received: " << value; +}, lifetime); + +// Without lifetime parameter - MUST store returned lifetime: +auto subscriptionLifetime = std::move(counter) | rpl::on_next([=](int value) { + // process value +}); +``` + +### Transforming Producers + +```cpp +auto strings = std::move(ints) | rpl::map([](int value) { + return QString::number(value * 2); +}); + +auto evenInts = std::move(ints) | rpl::filter([](int value) { + return (value % 2 == 0); +}); +``` + +### Combining Producers + +**`rpl::combine`** - combines latest values (lambdas receive unpacked arguments): + +```cpp +auto combined = rpl::combine(countProducer, textProducer); + +std::move(combined) | rpl::on_next([=](int count, const QString &text) { + qDebug() << "Count=" << count << ", Text=" << text; +}, lifetime); +``` + +**`rpl::merge`** - merges producers of same type: + +```cpp +auto merged = rpl::merge(sourceA, sourceB); + +std::move(merged) | rpl::on_next([=](QString &&value) { + qDebug() << "Merged value: " << value; +}, lifetime); +``` + +**Other pipeline starters** — besides `rpl::on_next`, there are: +- `rpl::on_error([=](Error &&e) { ... }, lifetime)` — handle errors +- `rpl::on_done([=] { ... }, lifetime)` — handle stream completion +- `rpl::on_next_error_done(nextCb, errorCb, doneCb, lifetime)` — handle all three + +The `Error` template parameter defaults to `rpl::no_error`: `rpl::producer`. + +**Key points:** +- Explicitly `std::move` producers when starting pipelines +- Pass `rpl::lifetime` to `on_...` methods or store returned lifetime +- Use `rpl::duplicate(producer)` to reuse a producer multiple times +- Combined producers automatically unpack tuples in lambdas (works with `rpl::map`, `rpl::filter`, and `rpl::on_next`) + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..f8564d18ce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Claude Code Pointer + +Read `AGENTS.md` and treat it as the canonical repository-wide instructions. diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000000..33345a7cb2 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,100 @@ +# Code Review Style Guide + +This file contains style and formatting rules that the review subagent must check and fix. These are mechanical issues that should always be caught during code review. + +## Empty line before closing brace + +Always add an empty line before the closing brace of a class (after all private fields): + +```cpp +// BAD: +class MyClass { +public: + void foo(); + +private: + int _value = 0; +}; + +// GOOD: +class MyClass { +public: + void foo(); + +private: + int _value = 0; + +}; +``` + +## Multi-line expressions — operators at the start of continuation lines + +When splitting an expression across multiple lines, place operators (like `&&`, `||`, `;`, `+`, etc.) at the **beginning** of continuation lines, not at the end of the previous line. This makes it immediately obvious from the left edge whether a line is a continuation or new code. + +```cpp +// BAD - continuation looks like scope code: +if (const auto &lottie = animation->lottie; + lottie && lottie->valid() && lottie->framesCount() > 1) { + lottie->animate([=] { + +// GOOD - semicolon at start signals continuation: +if (const auto &lottie = animation->lottie + ; lottie && lottie->valid() && lottie->framesCount() > 1) { + lottie->animate([=] { + +// BAD - trailing && makes next line look like independent code: +if (veryLongExpression() && + anotherLongExpression() && + anotherOne()) { + doSomething(); + +// GOOD - leading && clearly marks continuation: +if (veryLongExpression() + && anotherLongExpression() + && anotherOne()) { + doSomething(); +``` + +## Minimize type checks — prefer direct cast over is + as + +Don't check a type and then cast — just cast and check for null. `asUser()` already returns `nullptr` when the peer is not a user, so calling `isUser()` first is redundant. The same applies to `asChannel()`, `asChat()`, etc. + +```cpp +// BAD - redundant isUser() check, then asUser(): +if (peer && peer->isUser()) { + peer->asUser()->setNoForwardFlags( + +// GOOD - just cast and null-check: +if (const auto user = peer->asUser()) { + user->setNoForwardFlags( +``` + +When you need a specific subtype, look up the specific subtype directly instead of loading a generic type and then casting: + +```cpp +// BAD - loads generic peer, then casts: +if (const auto peer = session().data().peerLoaded(peerId) + ; peer && peer->isUser()) { + peer->asUser()->setNoForwardFlags( + +// GOOD - look up the specific subtype directly: +const auto userId = peerToUser(peerId); +if (const auto user = session().data().userLoaded(userId)) { + user->setNoForwardFlags( +``` + +Avoid C++17 `if` with initializer (`;` inside the condition) when the code can be written more clearly with simple nested `if` statements or by extracting the value beforehand: + +```cpp +// BAD - complex if-with-initializer: +if (const auto peer = session().data().peerLoaded(peerId) + ; peer && peer->isUser()) { + +// GOOD - simple nested ifs when direct lookup isn't available: +if (const auto peer = session().data().peerLoaded(peerId)) { + if (const auto user = peer->asUser()) { + +## 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. +``` diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 10d88423f5..82b1028a8a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -363,6 +363,8 @@ PRIVATE boxes/peers/add_bot_to_chat_box.h boxes/peers/add_participants_box.cpp boxes/peers/add_participants_box.h + boxes/peers/channel_ownership_transfer.cpp + boxes/peers/channel_ownership_transfer.h boxes/peers/choose_peer_box.cpp boxes/peers/choose_peer_box.h boxes/peers/edit_contact_box.cpp @@ -474,6 +476,8 @@ PRIVATE boxes/report_messages_box.h boxes/ringtones_box.cpp boxes/ringtones_box.h + boxes/select_future_owner_box.cpp + boxes/select_future_owner_box.h boxes/self_destruction_box.cpp boxes/self_destruction_box.h boxes/send_credits_box.cpp @@ -488,6 +492,12 @@ PRIVATE boxes/star_gift_auction_box.h boxes/star_gift_box.cpp boxes/star_gift_box.h + boxes/star_gift_cover_box.cpp + boxes/star_gift_cover_box.h + boxes/star_gift_craft_animation.cpp + boxes/star_gift_craft_animation.h + boxes/star_gift_craft_box.cpp + boxes/star_gift_craft_box.h boxes/star_gift_preview_box.cpp boxes/star_gift_preview_box.h boxes/star_gift_resale_box.cpp @@ -641,6 +651,17 @@ PRIVATE core/crash_reports.h core/credits_amount.h core/deadlock_detector.h + core/deep_links/deep_links_chats.cpp + core/deep_links/deep_links_chats.h + core/deep_links/deep_links_contacts.cpp + core/deep_links/deep_links_contacts.h + core/deep_links/deep_links_new.cpp + core/deep_links/deep_links_new.h + core/deep_links/deep_links_router.cpp + core/deep_links/deep_links_router.h + core/deep_links/deep_links_settings.cpp + core/deep_links/deep_links_settings.h + core/deep_links/deep_links_types.h core/file_utilities.cpp core/file_utilities.h core/launcher.cpp @@ -1031,6 +1052,8 @@ PRIVATE history/view/reactions/history_view_reactions_strip.h history/view/reactions/history_view_reactions_tabs.cpp history/view/reactions/history_view_reactions_tabs.h + history/view/history_view_top_peers_selector.cpp + history/view/history_view_top_peers_selector.h history/view/history_view_about_view.cpp history/view/history_view_about_view.h history/view/history_view_bottom_info.cpp @@ -1680,61 +1703,67 @@ PRIVATE settings/cloud_password/settings_cloud_password_step.h settings/cloud_password/settings_cloud_password_validate_icon.cpp settings/cloud_password/settings_cloud_password_validate_icon.h - settings/settings_active_sessions.cpp - settings/settings_active_sessions.h - settings/settings_advanced.cpp - settings/settings_advanced.h - settings/settings_blocked_peers.cpp - settings/settings_blocked_peers.h - settings/settings_business.cpp - settings/settings_business.h - settings/settings_chat.cpp - settings/settings_chat.h - settings/settings_calls.cpp - settings/settings_calls.h + settings/sections/settings_active_sessions.cpp + settings/sections/settings_active_sessions.h + settings/sections/settings_advanced.cpp + settings/sections/settings_advanced.h + settings/sections/settings_chat.cpp + settings/sections/settings_chat.h + settings/sections/settings_blocked_peers.cpp + settings/sections/settings_blocked_peers.h + settings/sections/settings_business.cpp + settings/sections/settings_business.h + settings/sections/settings_calls.cpp + settings/sections/settings_calls.h settings/settings_codes.cpp settings/settings_codes.h settings/settings_common_session.cpp settings/settings_common_session.h - settings/settings_credits.cpp - settings/settings_credits.h + settings/sections/settings_credits.cpp + settings/sections/settings_credits.h settings/settings_credits_graphics.cpp settings/settings_credits_graphics.h settings/settings_experimental.cpp settings/settings_experimental.h - settings/settings_folders.cpp - settings/settings_folders.h - settings/settings_global_ttl.cpp - settings/settings_global_ttl.h - settings/settings_information.cpp - settings/settings_information.h + settings/sections/settings_folders.cpp + settings/sections/settings_folders.h + settings/sections/settings_global_ttl.cpp + settings/sections/settings_global_ttl.h + settings/sections/settings_information.cpp + settings/sections/settings_information.h settings/settings_intro.cpp settings/settings_intro.h - settings/settings_local_passcode.cpp - settings/settings_local_passcode.h - settings/settings_main.cpp - settings/settings_main.h - settings/settings_notifications.cpp - settings/settings_notifications.h - settings/settings_notifications_type.cpp - settings/settings_notifications_type.h - settings/settings_passkeys.cpp - settings/settings_passkeys.h + settings/sections/settings_local_passcode.cpp + settings/sections/settings_local_passcode.h + settings/sections/settings_main.cpp + settings/sections/settings_main.h + settings/settings_search.cpp + settings/settings_search.h + settings/settings_faq_suggestions.cpp + settings/settings_faq_suggestions.h + settings/settings_builder.cpp + settings/settings_builder.h + settings/sections/settings_notifications.cpp + settings/sections/settings_notifications.h + settings/sections/settings_privacy_security.cpp + settings/sections/settings_privacy_security.h + settings/sections/settings_notifications_type.cpp + settings/sections/settings_notifications_type.h + settings/sections/settings_passkeys.cpp + settings/sections/settings_passkeys.h settings/settings_power_saving.cpp settings/settings_power_saving.h - settings/settings_premium.cpp - settings/settings_premium.h + settings/sections/settings_premium.cpp + settings/sections/settings_premium.h settings/settings_privacy_controllers.cpp settings/settings_privacy_controllers.h - settings/settings_privacy_security.cpp - settings/settings_privacy_security.h settings/settings_scale_preview.cpp settings/settings_scale_preview.h - settings/settings_shortcuts.cpp - settings/settings_shortcuts.h settings/settings_type.h - settings/settings_websites.cpp - settings/settings_websites.h + settings/sections/settings_shortcuts.cpp + settings/sections/settings_shortcuts.h + settings/sections/settings_websites.cpp + settings/sections/settings_websites.h storage/details/storage_file_utilities.cpp storage/details/storage_file_utilities.h storage/details/storage_settings_scheme.cpp diff --git a/Telegram/Resources/animations/chat/video_to_voice.tgs b/Telegram/Resources/animations/chat/video_to_voice.tgs new file mode 100644 index 0000000000..854a6c734c Binary files /dev/null and b/Telegram/Resources/animations/chat/video_to_voice.tgs differ diff --git a/Telegram/Resources/animations/chat/voice_to_video.tgs b/Telegram/Resources/animations/chat/voice_to_video.tgs new file mode 100644 index 0000000000..de864f07d9 Binary files /dev/null and b/Telegram/Resources/animations/chat/voice_to_video.tgs differ diff --git a/Telegram/Resources/animations/craft_failed.tgs b/Telegram/Resources/animations/craft_failed.tgs new file mode 100644 index 0000000000..a64ab0a7ff Binary files /dev/null and b/Telegram/Resources/animations/craft_failed.tgs differ diff --git a/Telegram/Resources/animations/craft_progress.tgs b/Telegram/Resources/animations/craft_progress.tgs new file mode 100644 index 0000000000..7bcdbda3b2 Binary files /dev/null and b/Telegram/Resources/animations/craft_progress.tgs differ diff --git a/Telegram/Resources/icons/chat/filled_forge.svg b/Telegram/Resources/icons/chat/filled_forge.svg new file mode 100644 index 0000000000..02b46ac481 --- /dev/null +++ b/Telegram/Resources/icons/chat/filled_forge.svg @@ -0,0 +1,7 @@ + + + General / filled_forge + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/chat/new_thread.svg b/Telegram/Resources/icons/chat/new_thread.svg new file mode 100644 index 0000000000..bcf13e4ce5 --- /dev/null +++ b/Telegram/Resources/icons/chat/new_thread.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/craft_chance.svg b/Telegram/Resources/icons/menu/craft_chance.svg new file mode 100644 index 0000000000..c3ea4c40db --- /dev/null +++ b/Telegram/Resources/icons/menu/craft_chance.svg @@ -0,0 +1,7 @@ + + + Icon / Menu / craft_chance + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/craft_random.svg b/Telegram/Resources/icons/menu/craft_random.svg new file mode 100644 index 0000000000..afaf213c10 --- /dev/null +++ b/Telegram/Resources/icons/menu/craft_random.svg @@ -0,0 +1,7 @@ + + + Icon / Menu / craft_random + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/craft_start.svg b/Telegram/Resources/icons/menu/craft_start.svg new file mode 100644 index 0000000000..5a8067fbbf --- /dev/null +++ b/Telegram/Resources/icons/menu/craft_start.svg @@ -0,0 +1,7 @@ + + + Icon / Menu / craft_forge + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/craft_tools.svg b/Telegram/Resources/icons/menu/craft_tools.svg new file mode 100644 index 0000000000..db111821bd --- /dev/null +++ b/Telegram/Resources/icons/menu/craft_tools.svg @@ -0,0 +1,7 @@ + + + Icon / Menu / craft_tools + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d2af09b841..23dc0e5293 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -460,6 +460,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_update_telegram" = "Update Telegram"; "lng_dlg_search_in" = "Search messages in"; "lng_dlg_search_from" = "From: {user}"; +"lng_chat_menu" = "Chat menu"; "lng_settings_save" = "Save"; "lng_settings_apply" = "Apply"; @@ -711,6 +712,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_shortcuts_record_round_message" = "Record Round Message"; "lng_shortcuts_admin_log" = "Group/Channel Recent Actions"; +"lng_attach" = "Add attachment"; +"lng_attach_replace" = "Replace attachment"; +"lng_emoji_sticker_gif" = "Choose emoji, sticker or gif"; +"lng_bot_keyboard_show" = "Show bot keyboard"; +"lng_bot_keyboard_hide" = "Hide bot keyboard"; +"lng_bot_commands_start" = "Bot commands start"; +"lng_broadcast_silent" = "Silent broadcast"; + "lng_settings_chat_reactions_title" = "Quick Reaction"; "lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction"; "lng_settings_chat_message_reply_from" = "Bob Harris"; @@ -787,6 +796,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_username_add" = "Add username"; "lng_settings_username_about" = "Username lets people contact you on Telegram without needing your phone number."; "lng_settings_birthday_label" = "Date of Birth"; +"lng_settings_birthday_title" = "Set your Birthday"; "lng_settings_birthday_add" = "Add"; "lng_settings_birthday_about" = "Choose who can see your birthday in {link}."; "lng_settings_birthday_about_link" = "Settings"; @@ -1247,6 +1257,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_faq_button" = "Go to FAQ"; "lng_settings_ask_ok" = "Ask a Volunteer"; "lng_settings_faq" = "Telegram FAQ"; +"lng_settings_faq_subtitle" = "FAQ"; "lng_settings_faq_link" = "https://telegram.org/faq#general-questions"; "lng_settings_features" = "Telegram Features"; "lng_settings_credits" = "My Stars"; @@ -1640,6 +1651,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_invite_upgrade_via_channel_about" = "You can send an invite link to the channel in a private message instead."; "lng_invite_status_disabled" = "available only to Premium users"; +"lng_leave_next_owner_box_title" = "Leave Channel?"; +"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_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"; +"lng_select_next_owner_box_title_admin" = "Appoint New Admin"; +"lng_select_next_owner_box_confirm" = "Appoint and Leave Group"; +"lng_select_next_owner_box_sub_admins" = "Channel admins"; +"lng_select_next_owner_box_sub_admins_group" = "Group admins"; +"lng_select_next_owner_box_sub_members" = "Channel members"; +"lng_select_next_owner_box_sub_members_group" = "Group members"; +"lng_select_next_owner_box_status_joined" = "joined {date}"; +"lng_select_next_owner_box_status_promoted" = "promoted {date}"; +"lng_select_next_owner_box_empty_list" = "There are no eligible participants to appoint as owner."; +"lng_select_next_owner_box_empty_list_admin" = "There are no eligible participants to appoint as admin."; + "lng_via_link_group_one" = "**{user}** restricts adding them to groups.\nYou can send them an invite link as message instead."; "lng_via_link_group_many#one" = "**{count} user** restricts adding them to groups.\nYou can send them an invite link as message instead."; "lng_via_link_group_many#other" = "**{count} users** restrict adding them to groups.\nYou can send them an invite link as message instead."; @@ -2321,6 +2350,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_displayed_self" = "You've started displaying {name} on your Telegram profile page."; "lng_action_gift_transferred_self_channel" = "You transferred a gift to {channel}"; "lng_action_gift_transferred_mine" = "You transferred a gift to {user}"; +"lng_action_gift_crafted" = "You crafted a new gift"; "lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}"; "lng_action_gift_sent_channel" = "{user} sent a gift to {name} for {cost}"; "lng_action_gift_sent_self_channel" = "You sent a gift to {name} for {cost}"; @@ -2328,6 +2358,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_self_auction" = "You've successfully bought a gift in the auction for {cost}."; "lng_action_gift_auction_won" = "You won the auction with a bid of {cost}."; "lng_action_gift_self_subtitle" = "Saved Gift"; +"lng_action_gift_crafted_subtitle" = "Crafted Gift"; "lng_action_gift_self_about#one" = "Display this gift on your page or convert it to **{count}** Star."; "lng_action_gift_self_about#other" = "Display this gift on your page or convert it to **{count}** Stars."; "lng_action_gift_self_about_unique" = "You can display this gift on your page or turn it into unique collectible and send to others."; @@ -2473,6 +2504,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_stake_game_won_you" = "You won {amount}"; "lng_action_stake_game_lost" = "{from} lost {amount}"; "lng_action_stake_game_lost_you" = "You lost {amount}"; +"lng_action_change_creator" = "{from} made {user} the new main admin of the group."; +"lng_action_new_creator_pending" = "{user} will become the new main admin in 7 days if {from} does not return."; "lng_stake_game_title" = "Emoji Stake"; "lng_stake_game_beta" = "Beta"; @@ -2713,6 +2746,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_badge" = "channel"; "lng_topic_author_badge" = "Topic Creator"; "lng_fast_reply" = "Reply"; +"lng_fast_share_tooltip" = "Right-click to select a Recent Contact."; "lng_cancel_edit_post_sure" = "Cancel editing?"; "lng_cancel_edit_post_yes" = "Yes"; "lng_cancel_edit_post_no" = "No"; @@ -2727,6 +2761,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_new_thread_title" = "New Thread"; "lng_bot_new_thread_about" = "Type any message to create a new thread."; "lng_bot_show_threads_list" = "Show Threads List"; +"lng_bot_off_thread_ph" = "Off-thread message"; "lng_attach_failed" = "Failed"; "lng_attach_file" = "File"; @@ -3754,6 +3789,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_stars_sold_out" = "sold out"; "lng_gift_stars_resale" = "resale"; "lng_gift_stars_on_sale" = "on sale"; +"lng_gift_on_sale_for" = "On sale for {price}"; "lng_gift_stars_premium" = "premium"; "lng_gift_stars_auction" = "auction"; "lng_gift_stars_auction_join" = "Join"; @@ -3811,12 +3847,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; "lng_gift_collectible_tag" = "gift"; +"lng_gift_burned_tag" = "burned"; +"lng_gift_crafted_tag" = "crafted"; +"lng_gift_uncommon_tag" = "uncommon"; +"lng_gift_rare_tag" = "rare"; +"lng_gift_epic_tag" = "epic"; +"lng_gift_legendary_tag" = "legendary"; "lng_gift_view_unpack" = "Unpack"; "lng_gift_anonymous_hint" = "Only you can see the sender's name."; "lng_gift_anonymous_hint_channel" = "Only admins of this channel can see the sender's name."; "lng_gift_hidden_hint" = "This gift is hidden. Only you can see it."; "lng_gift_hidden_unique" = "This gift is not displayed on your page."; "lng_gift_visible_hint" = "This gift is visible on your page."; +"lng_gift_burned_message" = "This gift was burned in crafting."; "lng_gift_hidden_hint_channel" = "This gift is hidden from visitors of your channel."; "lng_gift_visible_hint_channel" = "This gift is visible in your channel's Gifts."; "lng_gift_in_blockchain" = "This gift is in TON blockchain. {link}"; @@ -3853,6 +3896,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_unique_rarity" = "Only {percent} of such collectibles have this attribute."; "lng_gift_unique_sender" = "{from} sent you this gift on {date}"; "lng_gift_unique_sender_you" = "You bought this gift on {date}"; +"lng_gift_unique_crafter_you" = "You crafted this gift on {date}"; "lng_gift_unique_availability_label" = "Quantity"; "lng_gift_unique_availability#one" = "{count} of {amount} issued"; "lng_gift_unique_availability#other" = "{count} of {amount} issued"; @@ -4089,6 +4133,43 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_collection_reorder_exit" = "Apply Reorder"; "lng_gift_collection_remove_from" = "Remove from Collection"; "lng_gift_locked_title" = "Gift Locked"; +"lng_gift_craft_menu_button" = "Craft"; +"lng_gift_craft_info_title" = "Gift Crafting"; +"lng_gift_craft_info_about" = "Turn your gifts into rare, epic\nand legendary versions."; +"lng_gift_craft_rare_title" = "Get Rare Models"; +"lng_gift_craft_rare_about" = "Select up to 4 gifts to craft a new exclusive model."; +"lng_gift_craft_chance_title" = "Maximize Chances"; +"lng_gift_craft_chance_about" = "Combine more gifts to increase your odds of success."; +"lng_gift_craft_affect_title" = "Affect the Result"; +"lng_gift_craft_affect_about" = "Use gifts with the same attribute to boost its chance."; +"lng_gift_craft_start_button" = "Start Crafting"; +"lng_gift_craft_title" = "Craft Gift"; +"lng_gift_craft_about1" = "Add up to **4** gifts to craft new\n{gift}."; +"lng_gift_craft_about2" = "If crafting fails, all used gifts\nwill be lost."; +"lng_gift_craft_view_all" = "{emoji} View all craftable models {arrow}"; +"lng_gift_craft_chance_backdrop" = "{percent} chance the crafted gift will have **{name}** background."; +"lng_gift_craft_chance_symbol" = "{percent} chance the crafted gift will have **{name}** symbol."; +"lng_gift_craft_button" = "Craft {gift}"; +"lng_gift_craft_button_chance" = "{percent} Success Chance"; +"lng_gift_craft_select_title" = "Select Gifts"; +"lng_gift_craft_select_about" = "You need to add up to **3 gifts** from the same collection for crafting."; +"lng_gift_craft_select_your" = "Your gifts"; +"lng_gift_craft_select_none" = "You don't have other gifts from this collection."; +"lng_gift_craft_search_none" = "Could not find other gifts from this collection."; +"lng_gift_craft_select_market#one" = "{count} suitable gift on sale"; +"lng_gift_craft_select_market#other" = "{count} suitable gifts on sale"; +"lng_gift_craft_progress" = "Crafting"; +"lng_gift_craft_progress_about" = "Crafting **{gift}**"; +"lng_gift_craft_progress_chance" = "{percent} success chance"; +"lng_gift_craft_progress_fails" = "If crafting fails, all used\ngifts will be lost."; +"lng_gift_craft_failed" = "Crafting failed. Gifts consumed :("; +"lng_gift_craft_failed_title" = "Crafting Failed"; +"lng_gift_craft_failed_about#one" = "This crafting attempt was unsuccessful.\n**{count}** gift was lost."; +"lng_gift_craft_failed_about#other" = "This crafting attempt was unsuccessful.\n**{count}** gifts were lost."; +"lng_gift_craft_new_button" = "Craft New Gift"; +"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_auction_about_title" = "Auction"; "lng_auction_about_subtitle" = "Join the battle for exclusive gifts."; @@ -4211,6 +4292,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_auction_preview_models#one" = "This collectible features **{count}** unique model."; "lng_auction_preview_models#other" = "This collectible features **{count}** unique models."; "lng_auction_preview_models_button" = "Models"; +"lng_auction_preview_crafted#one" = "This collectible features **{count}** crafted model."; +"lng_auction_preview_crafted#other" = "This collectible features **{count}** crafted models."; +"lng_auction_preview_view_craftable" = "View craftable models {arrow}"; +"lng_auction_preview_view_primary" = "View primary models {arrow}"; "lng_auction_preview_backdrop" = "backdrop"; "lng_auction_preview_backdrops#one" = "This collectible features **{count}** unique backdrop."; "lng_auction_preview_backdrops#other" = "This collectible features **{count}** unique backdrops."; @@ -4457,6 +4542,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_record_listen_cancel_sure" = "Do you want to discard your recorded voice message?"; "lng_record_listen_cancel_sure_round" = "Do you want to discard your recorded video message?"; "lng_record_lock_discard" = "Discard"; +"lng_record_lock" = "Lock recording"; +"lng_record_lock_resume" = "Resume recording"; +"lng_record_lock_delete" = "Delete recording"; +"lng_record_lock_play" = "Play recording"; +"lng_record_lock_pause" = "Pause recording"; +"lng_record_cancel_recording" = "Cancel recording"; "lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message."; "lng_record_voice_tip" = "Hold to record audio. Click to switch to video."; "lng_record_video_tip" = "Hold to record video. Click to switch to audio."; @@ -4591,6 +4682,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_url_auth_open_confirm" = "Do you want to open {link}?"; "lng_url_auth_login_option" = "Log in to {domain} as {user}"; "lng_url_auth_allow_messages" = "Allow {bot} to send me messages"; +"lng_url_auth_login_title" = "Log in to {domain}"; +"lng_url_auth_login_button" = "Log in"; +"lng_url_auth_site_access" = "This site will receive your **name**, **username** and **profile photo**."; +"lng_url_auth_device_label" = "Device"; +"lng_url_auth_ip_label" = "IP Address"; +"lng_url_auth_login_attempt" = "This login attempt came from the device above."; +"lng_url_auth_allow_messages_label" = "Allow Messages"; +"lng_url_auth_allow_messages_about" = "This will allow {bot} to message you"; +"lng_url_auth_phone_sure_title" = "Phone Number"; +"lng_url_auth_phone_sure_text" = "{domain} wants to access your phone number **{phone}**.\n\nAllow access?"; +"lng_url_auth_phone_sure_deny" = "Deny"; +"lng_url_auth_phone_toast_good_title" = "Login successful"; +"lng_url_auth_phone_toast_good" = "You're now logged in to {domain}."; +"lng_url_auth_phone_toast_good_no_phone" = "You're now logged in to {domain}, but you didn't grant access to your phone number."; +"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_bot_start" = "Start"; "lng_bot_choose_group" = "Select a Group"; @@ -5008,6 +5116,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_grouped" = "Group items"; "lng_send_compressed_one" = "Compress the image"; "lng_send_compressed" = "Compress images"; +"lng_send_as_documents_one" = "Send as a document"; +"lng_send_as_documents" = "Send as documents"; "lng_send_media_invalid_files" = "Sorry, no valid files found."; "lng_send_image" = "Send an image"; "lng_send_file" = "Send as a file"; @@ -5158,6 +5268,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_link_move_down" = "Move Down"; "lng_link_shrink_photo" = "Shrink Photo"; "lng_link_enlarge_photo" = "Enlarge Photo"; +"lng_link_shrink_video" = "Shrink Video"; +"lng_link_enlarge_video" = "Enlarge Video"; "lng_link_remove" = "Do Not Preview"; "lng_link_about_choose" = "Click on a link to generate its preview."; @@ -5239,10 +5351,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_drag_images_here" = "Drop images here"; "lng_drag_photos_here" = "Drop photos here"; "lng_drag_files_here" = "Drop files here"; +"lng_drag_media_here" = "Drop photos and videos"; "lng_drag_to_send_quick" = "to send them in a quick way"; "lng_drag_to_send_no_compression" = "to send them without compression"; "lng_drag_to_send_files" = "to send them as documents"; +"lng_drag_to_send_media" = "to send them as media files"; "lng_selected_clear" = "Cancel"; "lng_selected_delete" = "Delete"; @@ -5985,6 +6099,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_transfer_channel" = "Transfer channel ownership"; "lng_rights_transfer_check" = "Security check"; "lng_rights_transfer_check_about" = "You can transfer this group to {user} only if you have:"; +"lng_rights_transfer_check_about_channel" = "You can transfer this channel to {user} only if you have:"; "lng_rights_transfer_check_password" = "• Enabled **Two-Step Verification** more than **7 days** ago."; "lng_rights_transfer_check_session" = "• Logged in on this device more than **24 hours** ago."; "lng_rights_transfer_check_later" = "Please come back later."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index a33a9ff016..1ab7e76e83 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -17,6 +17,8 @@ ../../animations/stats_earn.tgs ../../animations/voice_ttl_idle.tgs ../../animations/voice_ttl_start.tgs + ../../animations/chat/voice_to_video.tgs + ../../animations/chat/video_to_voice.tgs ../../animations/palette.tgs ../../animations/sleep.tgs ../../animations/greeting.tgs @@ -53,6 +55,7 @@ ../../animations/passkeys.tgs ../../animations/ban.tgs ../../animations/cocoon.tgs + ../../animations/craft_failed.tgs ../../animations/profile/profile_muting.tgs ../../animations/profile/profile_unmuting.tgs @@ -87,5 +90,6 @@ ../../animations/swipe_action/unpin.tgs ../../animations/swipe_action/read.tgs ../../animations/swipe_action/unread.tgs + ../../animations/craft_progress.tgs diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 82833358d5..3623e370ee 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="6.5.1.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 8202421b82..db9d1e7072 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 6,4,2,0 - PRODUCTVERSION 6,4,2,0 + FILEVERSION 6,5,1,0 + PRODUCTVERSION 6,5,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "6.4.2.0" + VALUE "FileVersion", "6.5.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2026" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "6.4.2.0" + VALUE "ProductVersion", "6.5.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index b67f32c362..cf1ab333a0 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 6,4,2,0 - PRODUCTVERSION 6,4,2,0 + FILEVERSION 6,5,1,0 + PRODUCTVERSION 6,5,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "6.4.2.0" + VALUE "FileVersion", "6.5.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2026" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "6.4.2.0" + VALUE "ProductVersion", "6.5.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 8a2a3cc090..af159e3486 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -482,7 +482,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { } break; case ButtonType::Auth: - UrlAuthBox::Activate(item, row, column); + UrlAuthBox::ActivateButton(controller->uiShow(), item, row, column); break; case ButtonType::UserProfile: { diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 3c5d35d2d8..3b6884dcd9 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -84,6 +84,8 @@ struct RemoteFileInfo { std::optional thumb; std::optional videoCover; std::vector attachedStickers; + bool forceFile = false; + }; } // namespace Api diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index f7331694bc..845aa03894 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -110,6 +110,7 @@ MTPInputMedia PrepareUploadedDocument( const auto flags = (spoiler ? Flag::f_spoiler : Flag()) | (info.thumb ? Flag::f_thumb : Flag()) | (item->groupId() ? Flag::f_nosound_video : Flag()) + | (info.forceFile ? Flag::f_force_file : Flag()) | (info.attachedStickers.empty() ? Flag::f_stickers : Flag()) | (ttlSeconds ? Flag::f_ttl_seconds : Flag()) | (info.videoCover ? Flag::f_video_cover : Flag()); diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 04bf454f34..f628d46e88 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -945,11 +945,15 @@ std::optional FromTL( .releasedBy = releasedBy, .themeUser = themeUser, .nanoTonForResale = FindTonForResale(data.vresell_amount()), + .craftChancePermille + = data.vcraft_chance_permille().value_or_empty(), .starsForResale = FindStarsForResale(data.vresell_amount()), .starsMinOffer = data.voffer_min_stars().value_or(-1), .number = data.vnum().v, .onlyAcceptTon = data.is_resale_ton_only(), .canBeTheme = data.is_theme_available(), + .crafted = data.is_crafted(), + .burned = data.is_burned(), .model = *model, .pattern = *pattern, .value = (data.vvalue_amount() @@ -999,6 +1003,7 @@ std::optional FromTL( unique->exportAt = data.vcan_export_at().value_or_empty(); unique->canTransferAt = data.vcan_transfer_at().value_or_empty(); unique->canResellAt = data.vcan_resell_at().value_or_empty(); + unique->canCraftAt = data.vcan_craft_at().value_or_empty(); } using Id = Data::SavedStarGiftId; const auto hasUnique = parsed->unique != nullptr; @@ -1038,6 +1043,20 @@ std::optional FromTL( }; } +int ParseRarity(const MTPStarGiftAttributeRarity &rarity) { + return rarity.match([&](const MTPDstarGiftAttributeRarity &data) { + return std::max(data.vpermille().v, 0); + }, [&](const MTPDstarGiftAttributeRarityUncommon &) { + return int(Data::UniqueGiftRarity::Uncommon); + }, [&](const MTPDstarGiftAttributeRarityRare &) { + return int(Data::UniqueGiftRarity::Rare); + }, [&](const MTPDstarGiftAttributeRarityEpic &) { + return int(Data::UniqueGiftRarity::Epic); + }, [&](const MTPDstarGiftAttributeRarityLegendary &) { + return int(Data::UniqueGiftRarity::Legendary); + }); +} + Data::UniqueGiftModel FromTL( not_null session, const MTPDstarGiftAttributeModel &data) { @@ -1045,7 +1064,7 @@ Data::UniqueGiftModel FromTL( .document = session->data().processDocument(data.vdocument()), }; result.name = qs(data.vname()); - result.rarityPermille = data.vrarity_permille().v; + result.rarityValue = ParseRarity(data.vrarity()); return result; } @@ -1057,14 +1076,14 @@ Data::UniqueGiftPattern FromTL( }; result.document->overrideEmojiUsesTextColor(true); result.name = qs(data.vname()); - result.rarityPermille = data.vrarity_permille().v; + result.rarityValue = ParseRarity(data.vrarity()); return result; } Data::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) { auto result = Data::UniqueGiftBackdrop{ .id = data.vbackdrop_id().v }; result.name = qs(data.vname()); - result.rarityPermille = data.vrarity_permille().v; + result.rarityValue = ParseRarity(data.vrarity()); result.centerColor = Ui::ColorFromSerialized( data.vcenter_color()); result.edgeColor = Ui::ColorFromSerialized( diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index aa07e586a0..3b9f64745b 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_user.h" #include "main/main_session.h" -#include "settings/settings_premium.h" // Settings::ShowPremium. +#include "settings/sections/settings_premium.h" // Settings::ShowPremium. namespace Api { namespace { diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 49bc010a8d..ca7de0d5b4 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -682,12 +682,7 @@ QString FormatReadDate(TimeId date, const QDateTime &now) { return tr::lng_mediaview_date_time( tr::now, lt_date, - tr::lng_month_day( - tr::now, - lt_month, - Lang::MonthDay(readDate.month())(tr::now), - lt_day, - QString::number(readDate.day())), + langDayOfMonthShort(readDate), lt_time, QLocale().toString(parsed.time(), "HH:mm:ss")); } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 821c37273f..b9550e7205 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -83,7 +83,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_prepare.h" #include "ui/toast/toast.h" #include "support/support_helper.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "storage/localimageloader.h" #include "storage/download_manager_mtproto.h" #include "storage/file_upload.h" @@ -3836,6 +3836,8 @@ void ApiWrap::editMedia( to.replyTo.monoforumPeerId = existing->sublistPeerId(); to.replaceMediaOf = MsgId(); } + const auto forceFile = (type == SendMediaType::File) + && (file.type == Ui::PreparedFile::Type::Video); _fileLoader->addTask(std::make_unique( &session(), file.path, @@ -3856,7 +3858,9 @@ void ApiWrap::editMedia( type, to, caption, - file.spoiler)); + file.spoiler, + nullptr, + forceFile)); } void ApiWrap::sendFiles( @@ -3889,6 +3893,8 @@ void ApiWrap::sendFiles( && type != SendMediaType::File) ? SendMediaType::Photo : SendMediaType::File; + const auto forceFile = (type == SendMediaType::File) + && (file.type == Ui::PreparedFile::Type::Video); tasks.push_back(std::make_unique( &session(), file.path, @@ -3911,7 +3917,8 @@ void ApiWrap::sendFiles( to, caption, file.spoiler, - album)); + album, + forceFile)); caption = TextWithTags(); } if (album) { diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 508b864a83..c6becf7bc1 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -1529,7 +1529,10 @@ void SetupChannelBox::firstCheckFail(UsernameResult result) { } } -EditNameBox::EditNameBox(QWidget*, not_null user) +EditNameBox::EditNameBox( + QWidget*, + not_null user, + Focus focus) : _user(user) , _api(&_user->session().mtp()) , _first( @@ -1542,7 +1545,8 @@ EditNameBox::EditNameBox(QWidget*, not_null user) st::defaultInputField, tr::lng_signup_lastname(), _user->lastName) -, _invertOrder(langFirstNameGoesSecond()) { +, _invertOrder(langFirstNameGoesSecond()) +, _focus(focus) { } void EditNameBox::prepare() { @@ -1567,21 +1571,22 @@ void EditNameBox::prepare() { _last->submits( ) | rpl::on_next([=] { submit(); }, _last->lifetime()); - _first->customTab(true); - _last->customTab(true); - _first->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { _last->setFocus(); + *handled = true; }, _first->lifetime()); _last->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { _first->setFocus(); + *handled = true; }, _last->lifetime()); } void EditNameBox::setInnerFocus() { - (_invertOrder ? _last : _first)->setFocusFast(); + const auto focusLast = (_focus == Focus::LastName) + || (_focus == Focus::FirstName && _invertOrder); + (focusLast ? _last : _first)->setFocusFast(); } void EditNameBox::submit() { diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index 4032653564..cb61dbdfd5 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -246,7 +246,14 @@ private: class EditNameBox : public Ui::BoxContent { public: - EditNameBox(QWidget*, not_null user); + enum class Focus { + FirstName, + LastName, + }; + EditNameBox( + QWidget*, + not_null user, + Focus focus = Focus::FirstName); protected: void setInnerFocus() override; @@ -266,6 +273,7 @@ private: object_ptr _last; bool _invertOrder = false; + Focus _focus = Focus::FirstName; mtpRequestId _requestId = 0; QString _sentName; diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index fd14f2043a..24cd9b852b 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -41,7 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "storage/file_upload.h" #include "storage/localimageloader.h" #include "window/window_session_controller.h" diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 3658f8c24e..c9e49481dc 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -809,6 +809,18 @@ backgroundConfirmCancel: RoundButton(backgroundConfirm) { urlAuthCheckbox: Checkbox(defaultBoxCheckbox) { width: 240px; } +urlAuthCheckboxAbout: FlatLabel(defaultPeerListAbout) { + style: TextStyle(boxTextStyle) { + font: font(12px); + } +} +urlAuthBoxRowTopLabel: FlatLabel(boxLabel) { + maxHeight: 30px; +} +urlAuthBoxRowBottomLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + maxHeight: 30px; +} addContactFieldMargin: margins(19px, 0px, 19px, 10px); addContactWarningMargin: margins(19px, 10px, 19px, 5px); @@ -1182,3 +1194,9 @@ moderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { }}; } } + +futureOwnerBox: Box(defaultBox) { + buttonPadding: margins(0px, 0px, 0px, 14px); + buttonHeight: 0px; +} +futureOwnerBoxSelect: collectibleBox; diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index af83512057..e7842c0e69 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_premium_limits.h" #include "data/data_session.h" +#include "data/data_channel.h" +#include "data/data_user.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -29,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" +#include "main/main_session_settings.h" #include "styles/style_dialogs.h" #include "styles/style_media_player.h" // mediaPlayerMenuCheck #include "styles/style_menu_icons.h" @@ -213,6 +216,17 @@ bool ChooseFilterValidator::canAdd() const { return false; } +bool ChooseFilterValidator::canAdd(FilterId filterId) const { + Expects(filterId != 0); + + const auto list = _history->owner().chatsFilters().list(); + const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); + if (i != end(list)) { + return !i->contains(_history); + } + return false; +} + bool ChooseFilterValidator::canRemove(FilterId filterId) const { Expects(filterId != 0); @@ -348,3 +362,197 @@ void FillChooseFilterMenu( menu->hideMenu(); }, menu->lifetime()); } + +bool FillChooseFilterWithAdminedGroupsMenu( + not_null controller, + not_null menu, + not_null user, + std::shared_ptr> listUpdates, + std::vector> common, + std::shared_ptr> collectCommon) { + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + const auto &list = session->data().chatsFilters().list(); + const auto showColors = session->data().chatsFilters().tagsEnabled(); + auto added = 0; + for (const auto &filter : list) { + const auto id = filter.id(); + if (!id) { + continue; + } + auto canRestrictList = std::vector>(); + const auto maybeAppend = [&](not_null chat) { + const auto channel = chat->peer->asChannel(); + if (channel && channel->canRestrictParticipant(user)) { + if (channel->isGroupAdmin(user) && !channel->amCreator()) { + return; + } + canRestrictList.push_back(chat->peer); + } + }; + for (const auto &chat : filter.always()) { + maybeAppend(chat); + } + for (const auto &chat : filter.pinned()) { + maybeAppend(chat); + } + if (canRestrictList.empty()) { + continue; + } + + const auto checked = std::make_shared(false); + + const auto contains = false; + const auto title = filter.title(); + auto item = base::make_unique_q( + menu->menu(), + menu->st().menu, + new QAction( + Ui::Text::FixAmpersandInAction(title.text.text), + menu.get()), + contains ? &st::mediaPlayerMenuCheck : nullptr, + contains ? &st::mediaPlayerMenuCheck : nullptr); + const auto triggered = [=, raw = item.get()] { + *checked = !*checked; + if (*checked) { + for (const auto &peer : canRestrictList) { + if (ranges::contains(common, peer)) { + collectCommon->push_back(peer->id); + } + } + } else { + for (const auto &peer : canRestrictList) { + if (const auto i = ranges::find(*collectCommon, peer->id); + i != collectCommon->end()) { + collectCommon->erase(i); + } + } + } + raw->Ui::Menu::Action::setIcon( + *checked ? &st::mediaPlayerMenuCheck : nullptr, + *checked ? &st::mediaPlayerMenuCheck : nullptr); + listUpdates->fire({}); + }; + item->setActionTriggered([=] { + triggered(); + + auto groups = session->settings().moderateCommonGroups(); + if (*checked && !ranges::contains(groups, id)) { + groups.push_back(id); + } else if (!*checked) { + groups.erase(ranges::remove(groups, id), groups.end()); + } + session->settings().setModerateCommonGroups(groups); + session->saveSettingsDelayed(); + }); + if (ranges::contains( + session->settings().moderateCommonGroups(), + id)) { + triggered(); + } + item->setPreventClose(true); + item->setMarkedText(title.text, QString(), Core::TextContext({ + .session = session, + .repaint = [raw = item.get()] { raw->update(); }, + .customEmojiLoopLimit = title.isStatic ? -1 : 0, + })); + + item->setIcon(Icon(showColors ? filter : filter.withColorIndex({}))); + menu->addAction(std::move(item)); + added++; + } + + session->data().chatsFilters().changed( + ) | rpl::on_next([=] { + menu->hideMenu(); + }, menu->lifetime()); + + return added; +} + +void SetupFilterDragAndDrop( + not_null outer, + not_null session, + Fn(QPoint)> filterIdAtPosition, + Fn 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))); + }; + const auto hasAction = [=](not_null drop, bool perform) { + const auto mimeData = drop->mimeData(); + const auto filterId = filterIdAtPosition( + outer->mapToGlobal(drop->pos())); + if (!filterId) { + return false; + } + const auto id = *filterId; + if (const auto h = historyFromMime(mimeData)) { + auto v = ChooseFilterValidator(h); + if (id) { + if (v.canAdd(id)) { + if (!v.limitReached(id, true).reached) { + if (perform) { + v.add(id); + } + return true; + } + } + } else { + if (const auto active = activeFilterId(); + active && v.canRemove(active)) { + if (perform) { + v.remove(active); + } + return true; + } + } + } + return false; + }; + outer->setAcceptDrops(true); + outer->events( + ) | rpl::filter([](not_null e) { + return e->type() == QEvent::DragEnter + || e->type() == QEvent::DragMove + || e->type() == QEvent::DragLeave + || e->type() == QEvent::Drop; + }) | rpl::on_next([=](not_null e) { + if (e->type() == QEvent::DragEnter) { + const auto de = static_cast(e.get()); + if (hasAction(de, false)) { + de->acceptProposedAction(); + } else { + de->ignore(); + } + } else if (e->type() == QEvent::DragMove) { + const auto dm = static_cast(e.get()); + if (hasAction(dm, false)) { + dm->acceptProposedAction(); + } else { + dm->ignore(); + } + } else if (e->type() == QEvent::DragLeave) { + } else if (e->type() == QEvent::Drop) { + const auto drop = static_cast(e.get()); + if (hasAction(drop, true)) { + drop->acceptProposedAction(); + } else { + drop->ignore(); + } + } + }, outer->lifetime()); +} diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index e6c5ad3353..99492c6ba6 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -7,7 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace Main { +class Session; +} // namespace Main + namespace Ui { +class RpWidget; class PopupMenu; } // namespace Ui @@ -26,6 +31,7 @@ public: }; [[nodiscard]] bool canAdd() const; + [[nodiscard]] bool canAdd(FilterId filterId) const; [[nodiscard]] bool canRemove(FilterId filterId) const; [[nodiscard]] LimitData limitReached( FilterId filterId, @@ -43,3 +49,17 @@ void FillChooseFilterMenu( not_null controller, not_null menu, not_null history); + +bool FillChooseFilterWithAdminedGroupsMenu( + not_null controller, + not_null menu, + not_null user, + std::shared_ptr> listUpdates, + std::vector> common, + std::shared_ptr> collectCommon); + +void SetupFilterDragAndDrop( + not_null outer, + not_null session, + Fn(QPoint)> filterIdAtPosition, + Fn activeFilterId); diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index c26d572b48..8902fc2c26 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/local_url_handlers.h" #include "lang/lang_keys.h" #include "main/main_account.h" +#include "main/main_session.h" #include "mtproto/facade.h" #include "settings/settings_common.h" #include "storage/localstorage.h" @@ -352,10 +353,12 @@ public: ProxiesBox( QWidget*, not_null controller, - Core::SettingsProxy &settings); + Core::SettingsProxy &settings, + const QString &highlightId = QString()); protected: void prepare() override; + void showFinished() override; void keyPressEvent(QKeyEvent *e) override; private: @@ -381,6 +384,10 @@ private: base::flat_map> _rows; + QPointer _addProxyButton; + QPointer _shareListButton; + QString _highlightId; + }; class ProxyBox final : public Ui::BoxContent { @@ -749,10 +756,12 @@ void ProxyRow::showMenu() { ProxiesBox::ProxiesBox( QWidget*, not_null controller, - Core::SettingsProxy &settings) + Core::SettingsProxy &settings, + const QString &highlightId) : _controller(controller) , _settings(settings) -, _initialWrap(this) { +, _initialWrap(this) +, _highlightId(highlightId) { _controller->views( ) | rpl::on_next([=](View &&view) { applyView(std::move(view)); @@ -774,13 +783,29 @@ void ProxiesBox::keyPressEvent(QKeyEvent *e) { void ProxiesBox::prepare() { setTitle(tr::lng_proxy_settings()); - addButton(tr::lng_proxy_add(), [=] { addNewProxy(); }); + _addProxyButton = addButton(tr::lng_proxy_add(), [=] { addNewProxy(); }); addButton(tr::lng_close(), [=] { closeBox(); }); setupTopButton(); setupContent(); } +void ProxiesBox::showFinished() { + if (_highlightId == u"proxy/add-proxy"_q) { + if (_addProxyButton) { + _highlightId = QString(); + Settings::HighlightWidget( + _addProxyButton, + { .rippleShape = true }); + } + } else if (_highlightId == u"proxy/share-list"_q) { + if (_shareListButton) { + _highlightId = QString(); + Settings::HighlightWidget(_shareListButton); + } + } +} + void ProxiesBox::setupTopButton() { const auto top = addTopButton(st::infoTopBarMenu); const auto menu @@ -790,7 +815,8 @@ void ProxiesBox::setupTopButton() { *menu = base::make_unique_q( top, st::popupMenuWithIcons); - const auto addAction = Ui::Menu::CreateAddActionCallback(*menu); + const auto raw = menu->get(); + const auto addAction = Ui::Menu::CreateAddActionCallback(raw); addAction({ .text = tr::lng_proxy_add_from_clipboard(tr::now), .handler = [=] { AddProxyFromClipboard(_controller, uiShow()); }, @@ -804,7 +830,16 @@ void ProxiesBox::setupTopButton() { .isAttention = true, }); } - (*menu)->popup(QCursor::pos()); + raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + top->setForceRippled(true); + raw->setDestroyedCallback([=] { + if (const auto strong = top.data()) { + strong->setForceRippled(false); + } + }); + raw->popup( + top->mapToGlobal( + QPoint(top->width(), top->height() - st::lineWidth * 3))); return true; }); } @@ -913,6 +948,7 @@ void ProxiesBox::setupContent() { tr::lng_proxy_edit_share_list_button(), st::settingsButton, { &st::menuIconCopy }); + _shareListButton = shareList; shareList->setClickedCallback([=] { _controller->shareItems(); }); @@ -1504,15 +1540,17 @@ void ProxiesBoxController::setupChecker(int id, const Checker &checker) { } object_ptr ProxiesBoxController::CreateOwningBox( - not_null account) { + not_null account, + const QString &highlightId) { auto controller = std::make_unique(account); - auto box = controller->create(); + auto box = controller->create(highlightId); Ui::AttachAsChild(box, std::move(controller)); return box; } -object_ptr ProxiesBoxController::create() { - auto result = Box(this, _settings); +object_ptr ProxiesBoxController::create( + const QString &highlightId) { + auto result = Box(this, _settings, highlightId); _show = result->uiShow(); for (const auto &item : _list) { updateView(item); @@ -1838,6 +1876,13 @@ void ProxiesBoxController::share(const ProxyData &proxy, bool qr) { _show->showToast(tr::lng_username_copied(tr::now)); } +void ProxiesBoxController::Show( + not_null controller, + const QString &highlightId) { + controller->show( + CreateOwningBox(&controller->session().account(), highlightId)); +} + ProxiesBoxController::~ProxiesBoxController() { if (_saveTimer.isActive()) { base::call_delayed( diff --git a/Telegram/SourceFiles/boxes/connection_box.h b/Telegram/SourceFiles/boxes/connection_box.h index 97636060c7..fcbe5ad6f6 100644 --- a/Telegram/SourceFiles/boxes/connection_box.h +++ b/Telegram/SourceFiles/boxes/connection_box.h @@ -47,8 +47,12 @@ public: const QMap &fields); static object_ptr CreateOwningBox( - not_null account); - object_ptr create(); + not_null account, + const QString &highlightId = QString()); + static void Show( + not_null controller, + const QString &highlightId = QString()); + object_ptr create(const QString &highlightId = QString()); enum class ItemState { Connecting, diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index d885b2ca06..1b7751b70b 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -246,7 +246,6 @@ Options::Option::Option( InitField(outer, _field, session); _field->setMaxLength(kOptionLimit + kErrorLimit); _field->show(); - _field->customTab(true); _wrap->hide(anim::type::instant); @@ -725,13 +724,14 @@ void Options::addEmptyOption() { _scrollToWidget.fire_copy(field); }, field->lifetime()); field->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { const auto index = findField(field); if (index + 1 < _list.size()) { _list[index + 1]->setFocus(); } else { _tabbed.fire({}); } + *handled = true; }, field->lifetime()); base::install_event_filter(field, [=](not_null event) { if (event->type() != QEvent::KeyPress @@ -863,7 +863,6 @@ not_null CreatePollBox::setupQuestion( InitField(getDelegate()->outerContainer(), question, session); question->setMaxLength(kQuestionLimit + kErrorLimit); question->setSubmitSettings(Ui::InputField::SubmitSettings::Both); - question->customTab(true); if (isPremium) { using Selector = ChatHelpers::TabbedSelector; @@ -968,7 +967,6 @@ not_null CreatePollBox::setupSolution( )); solution->setEditLinkCallback( DefaultEditLinkCallback(_controller->uiShow(), solution)); - solution->customTab(true); const auto warning = CreateWarningLabel( inner, @@ -1048,8 +1046,9 @@ object_ptr CreatePollBox::setupContent() { st::createPollLimitPadding)); question->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { options->focusFirst(); + *handled = true; }, question->lifetime()); Ui::AddSkip(container); @@ -1097,8 +1096,9 @@ object_ptr CreatePollBox::setupContent() { }, question->lifetime()); solution->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { question->setFocus(); + *handled = true; }, solution->lifetime()); quiz->setDisabled(_disabled & PollData::Flag::Quiz); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index cf4ae2ddcb..32d01b297d 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -841,7 +841,9 @@ void EditCaptionBox::setupDragArea() { auto computeState = [=](const QMimeData *data) { using DragState = Storage::MimeDataState; const auto state = Storage::ComputeMimeDataState(data); - return (state == DragState::PhotoFiles || state == DragState::Image) + return (state == DragState::PhotoFiles + || state == DragState::Image + || state == DragState::MediaFiles) ? (_asFile ? DragState::Files : DragState::Image) : state; }; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 0c1c8264b1..52ba4c7e2d 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -19,9 +19,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" -#include "settings/settings_premium.h" +#include "settings/settings_common.h" +#include "settings/sections/settings_premium.h" #include "settings/settings_privacy_controllers.h" -#include "settings/settings_privacy_security.h" +#include "settings/sections/settings_privacy_security.h" #include "ui/boxes/peer_qr_box.h" #include "ui/controls/invite_link_buttons.h" #include "ui/controls/invite_link_label.h" @@ -905,6 +906,8 @@ void EditPrivacyBox::setupContent() { { 0, st::settingsPrivacySkipTop, 0, 0 }); const auto always = addExceptionLink(Exception::Always); const auto never = addExceptionLink(Exception::Never); + _always = always->entity(); + _never = never->entity(); addLabel( content, _controller->exceptionsDescription() | rpl::map(tr::marked), @@ -952,9 +955,16 @@ void EditPrivacyBox::setupContent() { }, content->lifetime()); } +void EditPrivacyBox::showFinished() { + _window->checkHighlightControl(u"privacy/always"_q, _always.data()); + _window->checkHighlightControl(u"privacy/never"_q, _never.data()); + _controller->checkHighlightControls(_window); +} + void EditMessagesPrivacyBox( not_null box, - not_null controller) { + not_null controller, + const QString &highlightControlId) { box->setTitle(tr::lng_messages_privacy_title()); box->setWidth(st::boxWideWidth); @@ -971,6 +981,9 @@ void EditMessagesPrivacyBox( const auto inner = box->verticalLayout(); inner->add(object_ptr(box)); + auto highlightCharged = (Ui::RpWidget*)nullptr; + auto highlightRemoveFee = (Ui::RpWidget*)nullptr; + Ui::AddSkip(inner, st::messagePrivacyTopSkip); Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle()); const auto group = std::make_shared( @@ -1020,6 +1033,7 @@ void EditMessagesPrivacyBox( 0, st::messagePrivacyBottomSkip)) : nullptr; + highlightCharged = charged; struct State { rpl::variable stars; @@ -1069,6 +1083,7 @@ void EditMessagesPrivacyBox( tr::lng_messages_privacy_remove_fee(), std::move(label), st::settingsButtonNoIcon); + highlightRemoveFee = exceptions; const auto shower = exceptions->lifetime().make_state(); exceptions->setClickedCallback([=] { @@ -1166,6 +1181,18 @@ void EditMessagesPrivacyBox( box->closeBox(); }); } + + if (!highlightControlId.isEmpty()) { + box->showFinishes() | rpl::take(1) | rpl::on_next([=] { + if (highlightControlId == u"privacy/set-price"_q) { + Settings::HighlightWidget( + highlightCharged, + { .radius = st::boxRadius }); + } else if (highlightControlId == u"privacy/remove-fee"_q) { + Settings::HighlightWidget(highlightRemoveFee); + } + }, box->lifetime()); + } } rpl::producer SetupChargeSlider( diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index d46cbbfa8e..4265cba9b1 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -104,6 +104,10 @@ public: return nullptr; } + virtual void checkHighlightControls( + not_null controller) { + } + virtual ~EditPrivacyController() = default; protected: @@ -143,6 +147,7 @@ public: protected: void prepare() override; + void showFinished() override; private: bool showExceptionLink(Exception exception) const; @@ -163,12 +168,15 @@ private: const not_null _window; std::unique_ptr _controller; Value _value; + QPointer _always; + QPointer _never; }; void EditMessagesPrivacyBox( not_null box, - not_null controller); + not_null controller, + const QString &highlightControlId = QString()); [[nodiscard]] rpl::producer SetupChargeSlider( not_null container, diff --git a/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp index acf1f03859..59960a2f47 100644 --- a/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp @@ -303,8 +303,6 @@ Tasks::Task::Task( _field->show(); if (locked) { _field->setDisabled(true); - } else { - _field->customTab(true); } _wrap->hide(anim::type::instant); @@ -766,13 +764,14 @@ void Tasks::initTaskField(not_null task, TextWithEntities text) { _scrollToWidget.fire_copy(field); }, field->lifetime()); field->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { const auto index = findField(field); if (index + 1 < _list.size()) { _list[index + 1]->setFocus(); } else { _tabbed.fire({}); } + *handled = true; }, field->lifetime()); base::install_event_filter(field, [=](not_null event) { if (event->type() != QEvent::KeyPress @@ -933,7 +932,6 @@ not_null EditTodoListBox::setupTitle( InitField(getDelegate()->outerContainer(), title, session); title->setMaxLength(_titleLimit + kErrorLimit); title->setSubmitSettings(Ui::InputField::SubmitSettings::Both); - title->customTab(true); if (isPremium) { _emojiPanel = MakeEmojiPanel( @@ -1044,8 +1042,9 @@ object_ptr EditTodoListBox::setupContent() { st::createPollLimitPadding)); title->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { tasks->focusFirst(); + *handled = true; }, title->lifetime()); Ui::AddSkip(container); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 914dda90cc..88d437ae1c 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -45,7 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_checkout_process.h" #include "payments/payments_form.h" #include "settings/settings_credits_graphics.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/basic_click_handlers.h" // UrlClickHandler::Open. #include "ui/boxes/boost_box.h" // StartFireworks. #include "ui/boxes/confirm_box.h" @@ -65,6 +65,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/gradient_round_button.h" #include "ui/widgets/tooltip.h" @@ -222,7 +223,7 @@ using SpinnerState = Data::GiftUpgradeSpinner::State; const auto prefix = (use > 0) ? u"+"_q : QString(); const auto percent = Lang::FormatExactCountDecimal(use) + '%'; auto text = rpl::single(prefix + percent); - return MakeValueWithSmallButton(table, label, std::move(text)); + return MakeValueWithSmallButton(table, label, std::move(text)).widget; } [[nodiscard]] object_ptr MakeMinimumPriceValue( @@ -241,7 +242,7 @@ using SpinnerState = Data::GiftUpgradeSpinner::State; tr::bold(text.text), lt_gift, tr::bold(unique->title), - tr::marked)); + tr::marked)).widget; } [[nodiscard]] object_ptr MakeAveragePriceValue( @@ -260,7 +261,7 @@ using SpinnerState = Data::GiftUpgradeSpinner::State; tr::bold(text.text), lt_gift, tr::bold(unique->title), - tr::marked)); + tr::marked)).widget; } [[nodiscard]] object_ptr MakeAttributeValue( @@ -273,13 +274,25 @@ using SpinnerState = Data::GiftUpgradeSpinner::State; table->st().defaultValue); label->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto permille = attribute.rarityPermille; - auto text = rpl::single(QString::number(permille / 10.) + '%'); + const auto permille = attribute.rarityPermille(); + const auto rarity = attribute.rarityType(); + auto text = rpl::single(Data::UniqueGiftAttributeText(attribute)); const auto handler = [=](not_null button) { showTooltip(button, permille); }; - return MakeValueWithSmallButton(table, label, std::move(text), handler); + auto result = MakeValueWithSmallButton( + table, + label, + std::move(text), + handler); + if (rarity != Data::UniqueGiftRarity::Default) { + const auto colors = Data::UniqueGiftRarityBadgeColors(rarity); + result.button->setBrushOverride(colors.bg); + result.button->setTextFgOverride(colors.fg); + result.button->setRippleOverride(colors.bg); + } + return std::move(result.widget); } [[nodiscard]] object_ptr MakeAttributeValue( @@ -500,7 +513,7 @@ void AddUniqueGiftPropertyRows( const auto showRarity = [=](Data::GiftAttributeId id) { return [=]( not_null widget, - int rarity) { + int rarityPermille) { initVariants(); const auto weak = base::make_weak(widget); @@ -517,10 +530,11 @@ void AddUniqueGiftPropertyRows( id.type, unique)); } else if (const auto widget = weak.get()) { - const auto percent = QString::number(rarity / 10.) + '%'; + const auto percent = Data::UniqueGiftAttributeText( + { .rarityValue = rarityPermille }); showTooltip(widget, tr::lng_gift_unique_rarity( lt_percent, - rpl::single(TextWithEntities{ percent }), + rpl::single(tr::marked(percent)), tr::marked)); } }); @@ -615,7 +629,7 @@ void AddUniqueGiftPropertyRows( table, label.release(), std::move(text), - handler); + handler).widget; } [[nodiscard]] object_ptr MakeUniqueGiftValueValue( @@ -673,7 +687,7 @@ void AddUniqueGiftPropertyRows( table, label, tr::lng_gift_unique_value_learn_more(), - handler); + handler).widget; } void AddTable( @@ -1628,7 +1642,7 @@ void AddStarGiftTable( tr::lng_credits_box_history_entry_peer_in(), MakePeerTableValue(table, show, peerId, send, handler), st::giveawayGiftCodePeerMargin); - } else if (!entry.soldOutInfo) { + } else if (!entry.soldOutInfo && !giftToSelf) { AddTableRow( table, tr::lng_credits_box_history_entry_peer_in(), diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index 014481979b..965548afac 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_cloud_manager.h" #include "settings/settings_common.h" #include "spellcheck/spellcheck_types.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -1106,8 +1107,12 @@ Ui::ScrollToRequest Content::jump(int rows) { } // namespace -LanguageBox::LanguageBox(QWidget*, Window::SessionController *controller) -: _controller(controller) { +LanguageBox::LanguageBox( + QWidget*, + Window::SessionController *controller, + const QString &highlightId) +: _controller(controller) +, _highlightId(highlightId) { } void LanguageBox::prepare() { @@ -1180,6 +1185,22 @@ void LanguageBox::prepare() { }; } +void LanguageBox::showFinished() { + if (_controller && !_highlightId.isEmpty()) { + if (const auto window = Core::App().findWindow(this)) { + window->checkHighlightControl( + u"language/show-button"_q, + _showButtonToggle.data()); + window->checkHighlightControl( + u"language/translate-chats"_q, + _translateChatsToggle.data()); + window->checkHighlightControl( + u"language/do-not-translate"_q, + _doNotTranslateButton.data()); + } + } +} + void LanguageBox::setupTop(not_null container) { if (!_controller) { return; @@ -1190,6 +1211,7 @@ void LanguageBox::setupTop(not_null container) { tr::lng_translate_settings_show(), st::settingsButtonNoIcon))->toggleOn( rpl::single(Core::App().settings().translateButtonEnabled())); + _showButtonToggle = translateEnabled; translateEnabled->toggledValue( ) | rpl::filter([](bool checked) { @@ -1219,6 +1241,7 @@ void LanguageBox::setupTop(not_null container) { rpl::duplicate(premium), _1 && _2), _translateChatTurnOff.events())); + _translateChatsToggle = translateChat; std::move(premium) | rpl::on_next([=](bool value) { translateChat->setToggleLocked(!value); }, translateChat->lifetime()); @@ -1260,6 +1283,7 @@ void LanguageBox::setupTop(not_null container) { : Ui::LanguageName(list.front()); }), st::settingsButtonNoIcon); + _doNotTranslateButton = translateSkip; translateSkip->setClickedCallback([=] { uiShow()->showBox(Ui::EditSkipTranslationLanguages()); @@ -1299,7 +1323,9 @@ void LanguageBox::setInnerFocus() { _setInnerFocus(); } -base::binary_guard LanguageBox::Show(Window::SessionController *controller) { +base::binary_guard LanguageBox::Show( + Window::SessionController *controller, + const QString &highlightId) { auto result = base::binary_guard(); auto &manager = Lang::CurrentCloudManager(); @@ -1317,11 +1343,11 @@ base::binary_guard LanguageBox::Show(Window::SessionController *controller) { base::take(lifetime)->destroy(); } if (show) { - Ui::show(Box(weak.get())); + Ui::show(Box(weak.get(), highlightId)); } }, *lifetime); } else { - Ui::show(Box(controller)); + Ui::show(Box(controller, highlightId)); } manager.requestLanguageList(); diff --git a/Telegram/SourceFiles/boxes/language_box.h b/Telegram/SourceFiles/boxes/language_box.h index 350b570a68..d3b89a509f 100644 --- a/Telegram/SourceFiles/boxes/language_box.h +++ b/Telegram/SourceFiles/boxes/language_box.h @@ -24,14 +24,20 @@ class SessionController; class LanguageBox : public Ui::BoxContent { public: - LanguageBox(QWidget*, Window::SessionController *controller); + LanguageBox( + QWidget*, + Window::SessionController *controller, + const QString &highlightId = QString()); void setInnerFocus() override; - static base::binary_guard Show(Window::SessionController *controller); + [[nodiscard]] static base::binary_guard Show( + Window::SessionController *controller, + const QString &highlightId = QString()); protected: void prepare() override; + void showFinished() override; void keyPressEvent(QKeyEvent *e) override; @@ -40,6 +46,10 @@ private: [[nodiscard]] int rowsInPage() const; Window::SessionController *_controller = nullptr; + QString _highlightId; + QPointer _showButtonToggle; + QPointer _translateChatsToggle; + QPointer _doNotTranslateButton; rpl::event_stream _translateChatTurnOff; Fn _setInnerFocus; Fn _jump; diff --git a/Telegram/SourceFiles/boxes/local_storage_box.cpp b/Telegram/SourceFiles/boxes/local_storage_box.cpp index 4432a32349..3936aeb492 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.cpp +++ b/Telegram/SourceFiles/boxes/local_storage_box.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_common.h" #include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -132,6 +133,7 @@ public: void toggleProgress(bool shown); rpl::producer<> clearRequests() const; + [[nodiscard]] not_null clearButton() const; protected: int resizeGetHeight(int newWidth) override; @@ -210,6 +212,10 @@ rpl::producer<> LocalStorageBox::Row::clearRequests() const { return _clear->clicks() | rpl::to_empty; } +not_null LocalStorageBox::Row::clearButton() const { + return _clear.data(); +} + int LocalStorageBox::Row::resizeGetHeight(int newWidth) { const auto height = st::localStorageRowHeight; const auto padding = st::localStorageRowPadding; @@ -281,10 +287,13 @@ LocalStorageBox::LocalStorageBox( _timeLimit = settings.totalTimeLimit; } -void LocalStorageBox::Show(not_null controller) { +void LocalStorageBox::Show( + not_null controller, + const QString &highlightId) { auto shared = std::make_shared>( Box(&controller->session(), CreateTag())); const auto weak = shared->data(); + weak->_highlightId = highlightId; rpl::combine( controller->session().data().cache().statsOnMain(), controller->session().data().cacheBigFile().statsOnMain() @@ -306,6 +315,26 @@ void LocalStorageBox::prepare() { setupControls(); } +void LocalStorageBox::showFinished() { + if (_highlightId.isEmpty()) { + return; + } + if (_highlightId == u"storage/clear-cache"_q) { + if (const auto i = _rows.find(0); i != _rows.end()) { + Settings::HighlightWidget( + i->second->entity()->clearButton(), + { .rippleShape = true }); + } + } else if (_highlightId == u"storage/max-cache"_q) { + if (_totalSlider) { + const auto add = st::roundRadiusSmall; + Settings::HighlightWidget( + _totalSlider, + { .margin = { -add, -add, -add, -add }, .radius = add }); + } + } +} + void LocalStorageBox::updateRow( not_null*> row, const Database::TaggedSummary *data) { diff --git a/Telegram/SourceFiles/boxes/local_storage_box.h b/Telegram/SourceFiles/boxes/local_storage_box.h index cc9e01b571..b9de021e9a 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.h +++ b/Telegram/SourceFiles/boxes/local_storage_box.h @@ -44,10 +44,13 @@ public: not_null session, CreateTag); - static void Show(not_null controller); + static void Show( + not_null controller, + const QString &highlightId = QString()); protected: void prepare() override; + void showFinished() override; private: class Row; @@ -102,5 +105,6 @@ private: int64 _mediaSizeLimit = 0; size_type _timeLimit = 0; bool _limitsChanged = false; + QString _highlightId; }; diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index a95b0436b9..59ab073528 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -53,16 +53,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_window.h" +#include "window/window_session_controller.h" +#include "window/window_controller.h" +#include "boxes/choose_filter_box.h" #include "boxes/peer_list_box.h" #include "main/main_session_settings.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_action.h" +#include "base/qt/qt_key_modifiers.h" #include "ui/effects/round_checkbox.h" #include "styles/style_chat.h" +#include "styles/style_menu_icons.h" #include "styles/style_info.h" +#include "styles/style_media_player.h" // mediaPlayerMenuCheck base::options::toggle ModerateCommonGroups({ .id = kModerateCommonGroups, @@ -160,7 +168,9 @@ using CollectCommon = std::shared_ptr>; void FillMenuModerateCommonGroups( not_null menu, CommonGroups common, - CollectCommon collectCommon) { + CollectCommon collectCommon, + not_null user, + Fn onDestroyedCallback) { const auto resultList = menu->lifetime().make_state>(); const auto rememberCheckbox = Ui::CreateChild( @@ -172,18 +182,15 @@ void FillMenuModerateCommonGroups( st::historyHasCustomEmoji, st::historyHasCustomEmojiPosition, tr::lng_restrict_users_kick_from_common_group(tr::now, tr::rich)); - multiline->setDisabled(true); multiline->setAttribute(Qt::WA_TransparentForMouseEvents); menu->addAction(std::move(multiline)); const auto session = &common.front()->session(); + const auto settingsOnStart = session->settings().moderateCommonGroups(); + const auto checkboxesUpdate = std::make_shared>(); const auto save = [=] { auto result = std::vector( resultList->begin(), resultList->end()); - if (rememberCheckbox->checked()) { - session->settings().setModerateCommonGroups(result); - session->saveSettingsDelayed(); - } *collectCommon = std::move(result); }; for (const auto &group : common) { @@ -201,17 +208,22 @@ void FillMenuModerateCommonGroups( nullptr, nullptr); const auto state = item->lifetime().make_state(); - item->AbstractButton::setDisabled(true); - item->setActionTriggered([=, peerId = group->id] { - state->checkbox->setChecked(!state->checkbox->checked()); + const auto setChecked = [=, peerId = group->id](bool checked) { + state->checkbox->setChecked(checked); if (state->checkbox->checked()) { resultList->insert(peerId); } else { resultList->erase(peerId); } save(); + }; + item->setActionTriggered([=] { + setChecked(!state->checkbox->checked()); }); const auto raw = item.get(); + checkboxesUpdate->events() | rpl::on_next([=, peerId = group->id] { + setChecked(ranges::contains(*collectCommon, peerId)); + }, raw->lifetime()); state->checkboxWidget = Ui::CreateChild(raw); state->checkboxWidget->setAttribute(Qt::WA_TransparentForMouseEvents); state->checkboxWidget->resize(item->width() * 2, item->height()); @@ -224,10 +236,10 @@ void FillMenuModerateCommonGroups( ? int(size * Ui::ForumUserpicRadiusMultiplier()) : std::optional(); }); state->checkbox->setChecked( - ranges::contains( + /*ranges::contains( session->settings().moderateCommonGroups(), group->id) - || (collectCommon + || */(collectCommon && ranges::contains(*collectCommon, group->id)), anim::type::instant); state->checkboxWidget->paintOn([=](QPainter &p) { @@ -241,6 +253,29 @@ void FillMenuModerateCommonGroups( menu->addAction(std::move(item)); } menu->addSeparator(); + if (const auto window = Core::App().findWindow(menu->parentWidget())) { + auto hasActions = false; + Ui::Menu::CreateAddActionCallback(menu)(Ui::Menu::MenuCallback::Args{ + .text = tr::lng_restrict_users_kick_from_common_group(tr::now), + .handler = nullptr, + .icon = &st::menuIconAddToFolder, + .fillSubmenu = [&](not_null menu) { + hasActions = FillChooseFilterWithAdminedGroupsMenu( + window->sessionController(), + menu, + user, + checkboxesUpdate, + common, + collectCommon); + }, + .submenuSt = &st::foldersMenu, + }); + if (!hasActions) { + menu->removeAction(menu->actions().size() - 1); + menu->removeAction(menu->actions().size() - 1); // Separator. + } + } + menu->addSeparator(); { auto item = base::make_unique_q( menu->menu(), @@ -251,7 +286,7 @@ void FillMenuModerateCommonGroups( [] {}), nullptr, nullptr); - item->AbstractButton::setDisabled(true); + item->setPreventClose(true); item->setActionTriggered([=] { rememberCheckbox->setChecked(!rememberCheckbox->checked()); }); @@ -261,11 +296,18 @@ void FillMenuModerateCommonGroups( rememberCheckbox->show(); menu->addAction(std::move(item)); } + menu->setDestroyedCallback([=] { + onDestroyedCallback(); + if (!rememberCheckbox->checked()) { + session->settings().setModerateCommonGroups(settingsOnStart); + session->saveSettingsDelayed(); + } + }); } void ProccessCommonGroups( const HistoryItemsList &items, - Fn processHas) { + Fn)> processHas) { const auto moderateOptions = CalculateModerateOptions(items); if (moderateOptions.participants.size() != 1 || !moderateOptions.allCanBan) { @@ -298,7 +340,7 @@ void ProccessCommonGroups( } if (!filtered.empty()) { - processHas(filtered); + processHas(filtered, user); } }); } @@ -342,24 +384,79 @@ void CreateModerateMessagesBox( if (ModerateCommonGroups.value() || session->supportMode()) { ProccessCommonGroups( items, - crl::guard(box, [=](CommonGroups groups) { + crl::guard(box, [=](CommonGroups groups, not_null user) { using namespace Ui; const auto top = box->addTopButton(st::infoTopBarMenu); auto &lifetime = top->lifetime(); const auto menu = lifetime.make_state>(); + { + const auto was = collectCommon->size(); + *menu = base::make_unique_q( + top, + st::popupMenuExpandedSeparator); + FillMenuModerateCommonGroups( + *menu, + groups, + collectCommon, + user, + []{}); + *menu = nullptr; + if (was != collectCommon->size()) { + top->setIconOverride( + &st::infoTopBarMenuActive, + &st::infoTopBarMenuActive); + const auto minicheck = Ui::CreateChild(top); + minicheck->paintRequest() | rpl::on_next([=] { + auto p = Painter(minicheck); + const auto rect = minicheck->rect(); + const auto iconSize = QSize( + st::mediaPlayerMenuCheck.width(), + st::mediaPlayerMenuCheck.height()); + const auto scale = std::min( + rect.width() / float64(iconSize.width()), + rect.height() / float64(iconSize.height())); + if (scale < 1.0) { + p.save(); + p.translate(rect.center()); + p.scale(scale, scale); + p.translate(-rect.center()); + } + st::mediaPlayerMenuCheck.paintInCenter( + p, + rect, + st::windowActiveTextFg->c); + if (scale < 1.0) { + p.restore(); + } + }, minicheck->lifetime()); + minicheck->resize( + st::mediaPlayerMenuCheck.width() / 1.5, + st::mediaPlayerMenuCheck.width() / 1.5); + minicheck->show(); + minicheck->moveToLeft( + top->width() - st::lineWidth * 26, + top->height() - st::lineWidth * 29); + } + } + top->setClickedCallback([=] { top->setForceRippled(true); *menu = base::make_unique_q( top, st::popupMenuExpandedSeparator); - (*menu)->setDestroyedCallback([=, weak = top] { + const auto onDestroyedCallback = [=, weak = top] { if (const auto strong = weak.data()) { strong->setForceRippled(false); } - }); - FillMenuModerateCommonGroups(*menu, groups, collectCommon); + }; + FillMenuModerateCommonGroups( + *menu, + groups, + collectCommon, + user, + onDestroyedCallback); (*menu)->setForcedOrigin(PanelAnimation::Origin::TopRight); const auto point = QPoint(top->width(), top->height()); (*menu)->popup(top->mapToGlobal(point)); @@ -719,15 +816,7 @@ void CreateModerateMessagesBox( return result; }(); - Ui::AddSubsectionTitle( - inner, - rpl::conditional( - rpl::single(isSingle), - tr::lng_restrict_users_part_single_header(), - tr::lng_restrict_users_part_header( - lt_count, - rpl::single(participants.size()) | tr::to_count()))); - auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( + auto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions( box, prepareFlags, disabledMessages, @@ -739,6 +828,14 @@ void CreateModerateMessagesBox( Ui::AddSkip(container); Ui::AddDivider(container); Ui::AddSkip(container); + Ui::AddSubsectionTitle( + container, + rpl::conditional( + rpl::single(isSingle), + tr::lng_restrict_users_part_single_header(), + tr::lng_restrict_users_part_header( + lt_count, + rpl::single(participants.size()) | tr::to_count()))); container->add(std::move(checkboxes)); } @@ -752,6 +849,9 @@ void CreateModerateMessagesBox( const auto request = [=]( not_null peer, not_null channel) { + if (base::IsAltPressed() || base::IsCtrlPressed()) { + return; + } if (!kick) { Api::ChatParticipants::Restrict( channel, diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index 20bbb9f565..6e514a14b3 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -29,7 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rect.h" #include "passport/passport_encryption.h" #include "passport/passport_panel_edit_contact.h" -#include "settings/settings_privacy_security.h" +#include "settings/sections/settings_privacy_security.h" #include "styles/style_layers.h" #include "styles/style_passport.h" #include "styles/style_boxes.h" diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 9ed40d3e02..8f0c0a1d6c 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -362,6 +362,16 @@ const style::MultiSelect &PeerListController::computeSelectSt() const { return _selectSt ? *_selectSt : st::defaultMultiSelect; } +void PeerListController::showFinished() { + if (const auto onstack = _showFinished) { + onstack(); + } +} + +void PeerListController::setShowFinishedCallback(Fn callback) { + _showFinished = std::move(callback); +} + bool PeerListController::hasComplexSearch() const { return (_searchController != nullptr); } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 133019793f..6044bb70ed 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -492,8 +492,8 @@ public: virtual void prepare() = 0; - virtual void showFinished() { - } + virtual void showFinished(); + void setShowFinishedCallback(Fn callback); virtual void rowClicked(not_null row) = 0; virtual void rowMiddleClicked(not_null row) { @@ -627,6 +627,8 @@ private: const style::PeerList *_listSt = nullptr; const style::MultiSelect *_selectSt = nullptr; + Fn _showFinished; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index b32ce30ba3..acca09e832 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -11,7 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" // MessageMoneyRestriction. #include "base/random.h" #include "boxes/filters/edit_filter_chats_list.h" -#include "settings/settings_premium.h" +#include "settings/settings_common.h" +#include "settings/sections/settings_premium.h" #include "ui/boxes/confirm_box.h" #include "ui/effects/round_checkbox.h" #include "ui/text/text_utilities.h" @@ -62,7 +63,7 @@ constexpr auto kSearchPerPage = 50; } // namespace object_ptr PrepareContactsBox( - not_null sessionController) { + not_null window) { using Mode = ContactsBoxController::SortMode; class Controller final : public ContactsBoxController { public: @@ -90,7 +91,7 @@ object_ptr PrepareContactsBox( }; auto controller = std::make_unique( - &sessionController->session()); + &window->session()); controller->setStyleOverrides(&st::contactsWithStories); controller->setStoriesShown(true); const auto raw = controller.get(); @@ -105,7 +106,7 @@ object_ptr PrepareContactsBox( box->addButton(tr::lng_close(), [=] { box->closeBox(); }); box->addLeftButton( tr::lng_profile_add_contact(), - [=] { sessionController->showAddContact(); }); + [=] { window->showAddContact(); }); state->toggleSort = box->addTopButton(st::contactsSortButton, [=] { const auto online = (state->mode.current() == Mode::Online); const auto mode = online ? Mode::Alphabet : Mode::Online; @@ -118,8 +119,15 @@ object_ptr PrepareContactsBox( raw->setSortMode(Mode::Online); raw->wheelClicks() | rpl::on_next([=](not_null p) { - sessionController->showInNewWindow(p); + window->showInNewWindow(p); }, box->lifetime()); + + raw->setShowFinishedCallback([=] { + window->checkHighlightControl( + u"contacts/sort"_q, + state->toggleSort, + { .rippleShape = true }); + }); }; return Box(std::move(controller), std::move(init)); } diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.cpp b/Telegram/SourceFiles/boxes/peer_lists_box.cpp index 4506075cec..156d259fa0 100644 --- a/Telegram/SourceFiles/boxes/peer_lists_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_lists_box.cpp @@ -146,12 +146,12 @@ void PeerListsBox::updateScrollSkips() { } void PeerListsBox::prepare() { - auto rows = setInnerWidget( + _rows = setInnerWidget( object_ptr(this), st::boxScroll); for (auto &list : _lists) { - const auto content = rows->add(object_ptr( - rows, + const auto content = _rows->add(object_ptr( + _rows, list.controller.get())); list.content = content; list.delegate->setContent(content); @@ -176,13 +176,13 @@ void PeerListsBox::prepare() { } }, lifetime()); } - rows->resizeToWidth(firstController()->contentWidth()); + _rows->resizeToWidth(firstController()->contentWidth()); { setDimensions( firstController()->contentWidth(), std::clamp( - rows->height(), + _rows->height(), st::boxMaxListHeight, st::boxMaxListHeight * 3)); } @@ -350,6 +350,23 @@ void PeerListsBox::Delegate::peerListSetForeignRowChecked( } } +not_null PeerListsBox::addSeparatorBefore( + int listIndex, + object_ptr widget) { + Assert(!(listIndex < 0 || listIndex >= int(_lists.size()) || !_rows)); + const auto position = listIndex * 2; + auto wrapped = object_ptr>( + _rows, + std::move(widget)); + _lists[listIndex].content->heightValue( + ) | rpl::on_next([=, separator = wrapped.data()](int h) { + separator->toggle(h > 0, anim::type::instant); + }, wrapped->lifetime()); + _lists[listIndex].separator = wrapped.data(); + _rows->insert(position, std::move(wrapped)); + return _lists[listIndex].separator; +} + void PeerListsBox::Delegate::peerListScrollToTop() { _box->scrollToY(0); } diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.h b/Telegram/SourceFiles/boxes/peer_lists_box.h index 20377548a4..182af18963 100644 --- a/Telegram/SourceFiles/boxes/peer_lists_box.h +++ b/Telegram/SourceFiles/boxes/peer_lists_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_box.h" +namespace Ui { +class VerticalLayout; +} // namespace Ui + class PeerListsBox : public Ui::BoxContent { public: PeerListsBox( @@ -16,6 +20,10 @@ public: std::vector> controllers, Fn)> init); + not_null addSeparatorBefore( + int listIndex, + object_ptr widget); + [[nodiscard]] std::vector> collectSelectedRows(); protected: @@ -66,6 +74,7 @@ private: std::unique_ptr controller; std::unique_ptr delegate; PeerListContent *content = nullptr; + Ui::SlideWrap *separator = nullptr; }; friend class Delegate; @@ -99,5 +108,6 @@ private: std::vector _lists; Fn _init; bool _scrollBottomFixed = false; + Ui::VerticalLayout *_rows = nullptr; }; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 93983fb034..5355138ac8 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -39,7 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "main/main_session.h" #include "mtproto/mtproto_config.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/window_session_controller.h" #include "info/profile/info_profile_icon.h" #include "apiwrap.h" diff --git a/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp new file mode 100644 index 0000000000..990636ed77 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp @@ -0,0 +1,161 @@ +/* +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/channel_ownership_transfer.h" + +#include "api/api_cloud_password.h" +#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" +#include "ui/boxes/confirm_box.h" +#include "ui/layers/show.h" + +ChannelOwnershipTransfer::ChannelOwnershipTransfer( + not_null peer, + not_null selectedUser, + std::shared_ptr show, + Fn)> onSuccess) +: _peer(peer) +, _selectedUser(selectedUser) +, _show(show) +, _onSuccess(std::move(onSuccess)) { +} + +bool ChannelOwnershipTransfer::handleTransferPasswordError( + const QString &error) { + const auto session = &_selectedUser->session(); + auto about = (_peer->asChannel() && !_peer->isMegagroup() + ? tr::lng_rights_transfer_check_about_channel + : tr::lng_rights_transfer_check_about)( + tr::now, + lt_user, + tr::bold(_selectedUser->shortName()), + tr::marked); + if (auto box = PrePasswordErrorBox(error, session, std::move(about))) { + _show->showBox(std::move(box)); + return true; + } + return false; +} + +void ChannelOwnershipTransfer::start() { + if (const auto chat = _peer->asChatNotMigrated()) { + _peer->session().api().migrateChat(chat, [=]( + not_null channel) { + startTransfer(channel); + }); + } else if (const auto channel = _peer->asChannelOrMigrated()) { + startTransfer(channel); + } +} + +void ChannelOwnershipTransfer::startTransfer(not_null channel) { + const auto api = &channel->session().api(); + api->cloudPassword().reload(); + api->request(MTPchannels_EditCreator( + channel->inputChannel(), + MTP_inputUserEmpty(), + MTP_inputCheckPasswordEmpty() + )).fail([=](const MTP::Error &error) { + const auto &type = error.type(); + if (handleTransferPasswordError(type)) { + return; + } + const auto callback = crl::guard(_show->toastParent(), [=]( + Fn &&close) { + requestPassword(); + close(); + }); + _show->showBox(Ui::MakeConfirmBox({ + .text = tr::lng_rights_transfer_about( + tr::now, + lt_group, + tr::bold(channel->name()), + lt_user, + tr::bold(_selectedUser->shortName()), + tr::rich), + .confirmed = callback, + .confirmText = tr::lng_rights_transfer_sure(), + })); + }).send(); +} + +void ChannelOwnershipTransfer::requestPassword() { + _peer->session().api().cloudPassword().state( + ) | rpl::take( + 1 + ) | rpl::on_next([=](const Core::CloudPasswordState &state) { + auto fields = PasscodeBox::CloudFields::From(state); + fields.customTitle = tr::lng_rights_transfer_password_title(); + fields.customDescription + = tr::lng_rights_transfer_password_description(tr::now); + fields.customSubmitButton = tr::lng_passcode_submit(); + fields.customCheckCallback = [=]( + const Core::CloudPasswordResult &result, + base::weak_qptr box) { + sendRequest(box, result); + }; + _show->showBox(Box(&_peer->session(), fields)); + }, _lifetime); +} + +void ChannelOwnershipTransfer::sendRequest( + base::weak_qptr 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(), + _selectedUser->inputUser(), + result.result + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + const auto currentShow = box ? box->uiShow() : _show; + currentShow->showToast( + (channel->isBroadcast() + ? tr::lng_rights_transfer_done_channel + : tr::lng_rights_transfer_done_group)( + tr::now, + lt_user, + _selectedUser->shortName())); + if (_onSuccess) { + _onSuccess(currentShow); + } + }).fail([=](const MTP::Error &error) { + if (box && box->handleCustomCheckError(error)) { + return; + } + const auto &type = error.type(); + const auto problem = [&] { + if (type == u"CHANNELS_ADMIN_PUBLIC_TOO_MUCH"_q) { + return tr::lng_channels_too_much_public_other(tr::now); + } 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() + ? tr::lng_error_admin_limit_channel + : tr::lng_error_admin_limit)(tr::now); + } else if (type == u"CHANNEL_INVALID"_q) { + return (channel->isBroadcast() + ? tr::lng_channel_not_accessible + : tr::lng_group_not_accessible)(tr::now); + } + return Lang::Hard::ServerError(); + }(); + _show->showBox(Ui::MakeInformBox(problem)); + if (box) { + box->closeBox(); + } + }).handleFloodErrors().send(); +} diff --git a/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h new file mode 100644 index 0000000000..73c7709cc9 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h @@ -0,0 +1,48 @@ +/* +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 + +class ChannelData; +class UserData; + +namespace Ui { +class Show; +} // namespace Ui + +namespace Core { +struct CloudPasswordResult; +} // namespace Core + +class PasscodeBox; + +class ChannelOwnershipTransfer { +public: + ChannelOwnershipTransfer( + not_null peer, + not_null selectedUser, + std::shared_ptr show, + Fn)> onSuccess = nullptr); + + void start(); + +private: + bool handleTransferPasswordError(const QString &error); + void requestPassword(); + void sendRequest( + base::weak_qptr box, + const Core::CloudPasswordResult &result); + void startTransfer(not_null channel); + + const not_null _peer; + const not_null _selectedUser; + const std::shared_ptr _show; + const Fn)> _onSuccess; + + rpl::lifetime _lifetime; + +}; diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 3f5815771f..908579b221 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -352,7 +352,7 @@ void ChoosePeerBoxController::prepareRestrictions() { tr::lng_request_peer_requirements(), { 0, st::membersMarginTop, 0, 0 }); const auto skip = st::defaultSubsectionTitlePadding.left(); - auto separator = QString::fromUtf8("\n\xE2\x80\xA2 "); + auto separator = '\n' + Ui::kQBullet + ' '; raw->add( object_ptr( raw, diff --git a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp index ddd6e61b22..056b66d7f1 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp @@ -28,7 +28,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "editor/photo_editor_common.h" #include "editor/photo_editor_layer_widget.h" #include "history/view/controls/history_view_characters_limit.h" -#include "info/profile/info_profile_cover.h" +#include "info/profile/info_profile_values.h" +#include "ui/wrap/padding_wrap.h" #include "info/userpic/info_userpic_emoji_builder_common.h" #include "info/userpic/info_userpic_emoji_builder_menu_item.h" #include "lang/lang_keys.h" @@ -85,8 +86,7 @@ void SendRequest( const QString &first, const QString &last, const QString &phone, - const TextWithEntities ¬e, - Fn done) { + const TextWithEntities ¬e) { const auto wasContact = user->isContact(); using Flag = MTPcontacts_AddContact::Flag; user->session().api().request(MTPcontacts_AddContact( @@ -121,10 +121,97 @@ void SendRequest( } box->closeBox(); } - done(); }).send(); } +class Cover final : public Ui::FixedHeightWidget { +public: + Cover( + QWidget *parent, + not_null controller, + not_null user, + rpl::producer status); + +private: + void setupChildGeometry(); + void initViewers(rpl::producer status); + void refreshNameGeometry(int newWidth); + void refreshStatusGeometry(int newWidth); + + const style::InfoProfileCover &_st; + const not_null _user; + + object_ptr _userpic; + object_ptr _name = { nullptr }; + object_ptr _status = { nullptr }; + +}; + +Cover::Cover( + QWidget *parent, + not_null controller, + not_null user, + rpl::producer status) +: FixedHeightWidget(parent, st::infoEditContactCover.height) +, _st(st::infoEditContactCover) +, _user(user) +, _userpic( + this, + controller, + _user, + Ui::UserpicButton::Role::OpenPhoto, + Ui::UserpicButton::Source::PeerPhoto, + _st.photo) { + _userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + + _name = object_ptr(this, _st.name); + _name->setSelectable(true); + _name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now)); + + _status = object_ptr(this, _st.status); + _status->setAttribute(Qt::WA_TransparentForMouseEvents); + + initViewers(std::move(status)); + setupChildGeometry(); +} + +void Cover::setupChildGeometry() { + widthValue( + ) | rpl::on_next([this](int newWidth) { + _userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth); + refreshNameGeometry(newWidth); + refreshStatusGeometry(newWidth); + }, lifetime()); +} + +void Cover::initViewers(rpl::producer status) { + Info::Profile::NameValue( + _user + ) | rpl::on_next([=](const QString &name) { + _name->setText(name); + refreshNameGeometry(width()); + }, lifetime()); + + std::move( + status + ) | rpl::on_next([=](const QString &status) { + _status->setText(status); + refreshStatusGeometry(width()); + }, lifetime()); +} + +void Cover::refreshNameGeometry(int newWidth) { + auto nameWidth = newWidth - _st.nameLeft - _st.rightSkip; + _name->resizeToNaturalWidth(nameWidth); + _name->moveToLeft(_st.nameLeft, _st.nameTop, newWidth); +} + +void Cover::refreshStatusGeometry(int newWidth) { + auto statusWidth = newWidth - _st.statusLeft - _st.rightSkip; + _status->resizeToNaturalWidth(statusWidth); + _status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth); +} + class Controller { public: Controller( @@ -176,7 +263,6 @@ private: QString _phone; Fn _focus; Fn _save; - Fn()> _updatedPersonalPhoto; }; @@ -215,17 +301,15 @@ void Controller::setupContent() { } void Controller::setupCover() { - const auto cover = _box->addRow( - object_ptr( + _box->addRow( + object_ptr( _box, _window, _user, - Info::Profile::Cover::Role::EditContact, (_phone.isEmpty() ? tr::lng_contact_mobile_hidden() : rpl::single(Ui::FormatPhone(_phone)))), style::margins()); - _updatedPersonalPhoto = [=] { return cover->updatedPersonalPhoto(); }; } void Controller::setupNameFields() { @@ -301,21 +385,6 @@ void Controller::initNameFields( } } - const auto user = _user; - const auto personal = _updatedPersonalPhoto - ? _updatedPersonalPhoto() - : std::nullopt; - const auto done = [=] { - if (personal) { - if (personal->isNull()) { - user->session().api().peerPhoto().clearPersonal(user); - } else { - user->session().api().peerPhoto().upload( - user, - { base::duplicate(*personal) }); - } - } - }; const auto noteValue = _notesField ? [&] { auto textWithTags = _notesField->getTextWithAppliedMarkdown(); @@ -328,13 +397,12 @@ void Controller::initNameFields( : TextWithEntities(); SendRequest( base::make_weak(_box), - user, + _user, _sharePhone && _sharePhone->checked(), firstValue, lastValue, _phone, - noteValue, - done); + noteValue); }; const auto submit = [=] { const auto firstValue = first->getLastText().trimmed(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index cf66326954..1840710f69 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -24,10 +24,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/painter.h" #include "chat_helpers/emoji_suggestions_widget.h" -#include "settings/settings_privacy_security.h" +#include "settings/sections/settings_privacy_security.h" #include "ui/boxes/choose_date_time.h" #include "ui/boxes/confirm_box.h" -#include "boxes/passcode_box.h" +#include "boxes/peers/channel_ownership_transfer.h" #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" @@ -35,10 +35,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" -#include "core/core_cloud_password.h" #include "base/unixtime.h" #include "apiwrap.h" -#include "api/api_cloud_password.h" #include "main/main_session.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -368,7 +366,7 @@ void EditAdminBox::prepare() { .anyoneCanAddMembers = anyoneCanAddMembers, }; Ui::AddSubsectionTitle(inner, tr::lng_rights_edit_admin_header()); - auto [checkboxes, getChecked, changes] = CreateEditAdminRights( + auto [checkboxes, getChecked, changes, highlightWidget] = CreateEditAdminRights( inner, prepareFlags, disabledMessages, @@ -587,155 +585,12 @@ not_null*> EditAdminBox::setupTransferButton( } void EditAdminBox::transferOwnership() { - if (_checkTransferRequestId) { - return; - } - - const auto channel = peer()->isChannel() - ? peer()->asChannel()->inputChannel() - : MTP_inputChannelEmpty(); - const auto api = &peer()->session().api(); - api->cloudPassword().reload(); - _checkTransferRequestId = api->request(MTPchannels_EditCreator( - channel, - MTP_inputUserEmpty(), - MTP_inputCheckPasswordEmpty() - )).fail([=](const MTP::Error &error) { - _checkTransferRequestId = 0; - if (!handleTransferPasswordError(error.type())) { - const auto callback = crl::guard(this, [=](Fn &&close) { - transferOwnershipChecked(); - close(); - }); - getDelegate()->show(Ui::MakeConfirmBox({ - .text = tr::lng_rights_transfer_about( - tr::now, - lt_group, - tr::bold(peer()->name()), - lt_user, - tr::bold(user()->shortName()), - tr::rich), - .confirmed = callback, - .confirmText = tr::lng_rights_transfer_sure(), - })); - } - }).send(); -} - -bool EditAdminBox::handleTransferPasswordError(const QString &error) { - const auto session = &user()->session(); - auto about = tr::lng_rights_transfer_check_about( - tr::now, - lt_user, - tr::bold(user()->shortName()), - tr::marked); - if (auto box = PrePasswordErrorBox(error, session, std::move(about))) { - getDelegate()->show(std::move(box)); - return true; - } - return false; -} - -void EditAdminBox::transferOwnershipChecked() { - if (const auto chat = peer()->asChatNotMigrated()) { - peer()->session().api().migrateChat(chat, crl::guard(this, [=]( - not_null channel) { - requestTransferPassword(channel); - })); - } else if (const auto channel = peer()->asChannelOrMigrated()) { - requestTransferPassword(channel); - } else { - Unexpected("Peer in SaveAdminCallback."); - } - -} - -void EditAdminBox::requestTransferPassword(not_null channel) { - peer()->session().api().cloudPassword().state( - ) | rpl::take( - 1 - ) | rpl::on_next([=](const Core::CloudPasswordState &state) { - auto fields = PasscodeBox::CloudFields::From(state); - fields.customTitle = tr::lng_rights_transfer_password_title(); - fields.customDescription - = tr::lng_rights_transfer_password_description(tr::now); - fields.customSubmitButton = tr::lng_passcode_submit(); - fields.customCheckCallback = crl::guard(this, [=]( - const Core::CloudPasswordResult &result, - base::weak_qptr box) { - sendTransferRequestFrom(box, channel, result); - }); - getDelegate()->show(Box(&channel->session(), fields)); - }, lifetime()); -} - -void EditAdminBox::sendTransferRequestFrom( - base::weak_qptr box, - not_null channel, - const Core::CloudPasswordResult &result) { - if (_transferRequestId) { - return; - } - const auto weak = base::make_weak(this); - const auto user = this->user(); - const auto api = &channel->session().api(); - _transferRequestId = api->request(MTPchannels_EditCreator( - channel->inputChannel(), - user->inputUser(), - result.result - )).done([=](const MTPUpdates &result) { - api->applyUpdates(result); - if (!box && !weak) { - return; - } - const auto show = box ? box->uiShow() : weak->uiShow(); - show->showToast( - (channel->isBroadcast() - ? tr::lng_rights_transfer_done_channel - : tr::lng_rights_transfer_done_group)( - tr::now, - lt_user, - user->shortName())); - show->hideLayer(); - }).fail(crl::guard(this, [=](const MTP::Error &error) { - if (weak) { - _transferRequestId = 0; - } - if (box && box->handleCustomCheckError(error)) { - return; - } - - const auto &type = error.type(); - const auto problem = [&] { - if (type == u"CHANNELS_ADMIN_PUBLIC_TOO_MUCH"_q) { - return tr::lng_channels_too_much_public_other(tr::now); - } 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() - ? tr::lng_error_admin_limit_channel - : tr::lng_error_admin_limit)(tr::now); - } else if (type == u"CHANNEL_INVALID"_q) { - return (channel->isBroadcast() - ? tr::lng_channel_not_accessible - : tr::lng_group_not_accessible)(tr::now); - } - return Lang::Hard::ServerError(); - }(); - const auto recoverable = [&] { - return (type == u"PASSWORD_MISSING"_q) - || type.startsWith(u"PASSWORD_TOO_FRESH_"_q) - || type.startsWith(u"SESSION_TOO_FRESH_"_q); - }(); - const auto weak = base::make_weak(this); - getDelegate()->show(Ui::MakeInformBox(problem)); - if (box) { - box->closeBox(); - } - if (weak && !recoverable) { - closeBox(); - } - })).handleFloodErrors().send(); + _ownershipTransfer = std::make_unique( + peer(), + user(), + uiShow(), + [](std::shared_ptr show) { show->hideLayer(); }); + _ownershipTransfer->start(); } EditRestrictedBox::EditRestrictedBox( @@ -799,7 +654,7 @@ void EditRestrictedBox::prepare() { Ui::AddSubsectionTitle( verticalLayout(), tr::lng_rights_user_restrictions_header()); - auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( + auto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions( this, prepareFlags, disabledMessages, diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h index b67efcd140..79f9f6294b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h @@ -22,11 +22,7 @@ template class SlideWrap; } // namespace Ui -namespace Core { -struct CloudPasswordResult; -} // namespace Core - -class PasscodeBox; +class ChannelOwnershipTransfer; class EditParticipantBox : public Ui::BoxContent { public: @@ -100,13 +96,6 @@ private: not_null addRankInput( not_null container); void transferOwnership(); - void transferOwnershipChecked(); - bool handleTransferPasswordError(const QString &error); - void requestTransferPassword(not_null channel); - void sendTransferRequestFrom( - base::weak_qptr box, - not_null channel, - const Core::CloudPasswordResult &result); bool canSave() const { return _saveCallback != nullptr; } @@ -128,14 +117,15 @@ private: Ui::Checkbox *_addAsAdmin = nullptr; Ui::SlideWrap *_adminControlsWrap = nullptr; Ui::InputField *_rank = nullptr; - mtpRequestId _checkTransferRequestId = 0; - mtpRequestId _transferRequestId = 0; + Fn _save, _finishSave; TimeId _promotedSince = 0; UserData *_by = nullptr; std::optional _addingBot; + std::unique_ptr _ownershipTransfer; + }; // Restricted box works with flags in the opposite way. diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index c950c1f19f..e41de5d728 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -47,7 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_single_player.h" #include "main/main_session.h" #include "settings/settings_common.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/boxes/boost_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -1096,7 +1096,12 @@ void Apply( return result; } -Fn AddColorGiftTabs( +struct ColorGiftTabsResult { + Fn switchToNext; + QPointer tabs; +}; + +ColorGiftTabsResult AddColorGiftTabs( not_null container, not_null session, Fn chosen, @@ -1163,14 +1168,17 @@ Fn AddColorGiftTabs( container->resizeToWidth(container->width()); }, container->lifetime()); - return [=]() { - const auto &list = state->list.current(); - if (!list.empty()) { - if (state->tabs) { - state->tabs->setActiveTab(QString::number(list.front().id)); + return { + .switchToNext = [=]() { + const auto &list = state->list.current(); + if (!list.empty()) { + if (state->tabs) { + state->tabs->setActiveTab(QString::number(list.front().id)); + } + chosen(list.front().id); } - chosen(list.front().id); - } + }, + .tabs = state->tabs, }; } @@ -1721,6 +1729,12 @@ void AddLevelBadge( }, badge->lifetime()); } +struct ColorSectionHighlights { + QPointer emojiButton; + QPointer resetButton; + QPointer giftTabs; +}; + void EditPeerColorSection( not_null box, not_null container, @@ -1728,7 +1742,8 @@ void EditPeerColorSection( std::shared_ptr show, not_null peer, std::shared_ptr style, - std::shared_ptr theme) { + std::shared_ptr theme, + ColorSectionHighlights *highlights) { ProcessButton(button); const auto group = peer->isMegagroup(); @@ -1939,7 +1954,7 @@ void EditPeerColorSection( const auto iconInner = iconWrap->entity(); Ui::AddSkip(iconInner, st::settingsColorSampleSkip); - iconInner->add(CreateEmojiIconButton( + const auto emojiButton = iconInner->add(CreateEmojiIconButton( iconInner, show, style, @@ -1948,6 +1963,9 @@ void EditPeerColorSection( state->emojiId.value(), [=](DocumentId id) { state->emojiId = id; }, false)); + if (highlights) { + highlights->emojiButton = emojiButton; + } Ui::AddSkip(iconInner, st::settingsColorSampleSkip); Ui::AddDividerText( @@ -2035,11 +2053,14 @@ void EditPeerColorSection( Ui::AddSkip(container, st::settingsColorSampleSkip); const auto session = &peer->session(); - const auto switchToNextTab = AddColorGiftTabs( + const auto giftTabs = AddColorGiftTabs( container, session, [=](uint64 giftId) { state->showingGiftId = giftId; }, false); + if (highlights) { + highlights->giftTabs = giftTabs.tabs; + } auto showingGiftId = state->showingGiftId.value(); AddGiftSelector( @@ -2066,7 +2087,7 @@ void EditPeerColorSection( }), false, rpl::single(uint64(0)), - switchToNextTab); + giftTabs.switchToNext); } button->setClickedCallback([=] { @@ -2178,7 +2199,8 @@ void EditPeerProfileColorSection( not_null peer, std::shared_ptr style, std::shared_ptr theme, - Fn aboutCallback) { + Fn aboutCallback, + ColorSectionHighlights *highlights) { Expects(peer->isSelf()); ProcessButton(button); @@ -2219,7 +2241,9 @@ void EditPeerProfileColorSection( ? std::nullopt : std::make_optional(state->patternEmojiId.current())); }; - setIndex(peer->colorProfileIndex().value_or(kUnsetColorIndex)); + setIndex(peer->emojiStatusId().collectible + ? kUnsetColorIndex + : peer->colorProfileIndex().value_or(kUnsetColorIndex)); const auto margin = st::settingsColorRadioMargin; const auto skip = st::settingsColorRadioSkip; @@ -2236,7 +2260,7 @@ void EditPeerProfileColorSection( { margin, skip, margin, skip }); Ui::AddSkip(container, st::settingsColorSampleSkip); - container->add(CreateEmojiIconButton( + const auto emojiButton = container->add(CreateEmojiIconButton( container, show, style, @@ -2249,6 +2273,9 @@ void EditPeerProfileColorSection( resetUnique(); }, true)); + if (highlights) { + highlights->emojiButton = emojiButton; + } const auto resetWrap = container->add( object_ptr>( @@ -2262,6 +2289,9 @@ void EditPeerProfileColorSection( resetInner, tr::lng_settings_color_reset(), st::settingsButtonLightNoIcon)); + if (highlights) { + highlights->resetButton = resetButton; + } resetButton->setClickedCallback([=] { state->index = kUnsetColorIndex; state->patternEmojiId = 0; @@ -2305,11 +2335,14 @@ void EditPeerProfileColorSection( Ui::AddSkip(container, st::settingsColorSampleSkip); const auto session = &peer->session(); - const auto switchToNextTab = AddColorGiftTabs( + const auto giftTabs = AddColorGiftTabs( container, session, [=](uint64 giftId) { state->showingGiftId = giftId; }, true); + if (highlights) { + highlights->giftTabs = giftTabs.tabs; + } auto showingGiftId = state->showingGiftId.value(); AddGiftSelector( @@ -2337,7 +2370,7 @@ void EditPeerProfileColorSection( }), true, state->selectedGiftId.value(), - switchToNextTab); + giftTabs.switchToNext); } struct ProfileState { @@ -2434,10 +2467,21 @@ void EditPeerProfileColorSection( void EditPeerColorBox( not_null box, - std::shared_ptr show, + not_null controller, not_null peer, std::shared_ptr style, - std::shared_ptr theme) { + std::shared_ptr theme, + PeerColorTab initialTab) { + const auto show = controller->uiShow(); + if (!style) { + style = std::make_shared( + peer->session().colorIndicesValue()); + } + if (!theme) { + theme = std::shared_ptr( + Window::Theme::DefaultChatThemeOn(box->lifetime())); + style->apply(theme.get()); + } box->setTitle(peer->isSelf() ? tr::lng_settings_color_title() : tr::lng_edit_channel_color()); @@ -2450,7 +2494,7 @@ void EditPeerColorBox( const auto button = box->addButton( tr::lng_settings_color_apply(), [] {}); - EditPeerColorSection(box, box->verticalLayout(), button, show, peer, style, theme); + EditPeerColorSection(box, box->verticalLayout(), button, show, peer, style, theme, nullptr); return; } const auto buttonContainer = box->addButton( @@ -2516,6 +2560,12 @@ void EditPeerColorBox( content->add(std::move(profileOwned)); content->add(std::move(nameOwned)); + struct HighlightState { + ColorSectionHighlights profile; + ColorSectionHighlights name; + }; + const auto highlightState = box->lifetime().make_state(); + EditPeerProfileColorSection( box, profile, @@ -2524,9 +2574,38 @@ void EditPeerColorBox( peer, style, theme, - [=] { switchTab(1); }); + [=] { switchTab(1); }, + &highlightState->profile); - EditPeerColorSection(box, name, nameButton, show, peer, style, theme); + EditPeerColorSection( + box, + name, + nameButton, + show, + peer, + style, + theme, + &highlightState->name); + + if (initialTab == PeerColorTab::Name) { + switchTab(1); + } + + box->setShowFinishedCallback([=] { + const auto isProfileTab = (initialTab == PeerColorTab::Profile); + const auto &highlights = isProfileTab + ? highlightState->profile + : highlightState->name; + controller->checkHighlightControl( + u"profile-color/add-icons"_q, + highlights.emojiButton.data()); + controller->checkHighlightControl( + u"profile-color/use-gift"_q, + highlights.giftTabs.data()); + controller->checkHighlightControl( + u"profile-color/reset"_q, + highlights.resetButton.data()); + }); } void SetupPeerColorSample( @@ -2716,7 +2795,7 @@ void SetupPeerColorSample( emojiStatusWidget->setAttribute(Qt::WA_TransparentForMouseEvents); } -void AddPeerColorButton( +not_null AddPeerColorButton( not_null container, std::shared_ptr show, not_null peer, @@ -2767,8 +2846,17 @@ void AddPeerColorButton( } button->setClickedCallback([=] { - show->show(Box(EditPeerColorBox, show, peer, style, theme)); + if (const auto controller = show->resolveWindow()) { + controller->show(Box( + EditPeerColorBox, + controller, + peer, + style, + theme, + PeerColorTab::Profile)); + } }); + return button; } void CheckBoostLevel( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h index 394a13aa88..1dcad992e4 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h @@ -19,6 +19,10 @@ namespace ChatHelpers { class Show; } // namespace ChatHelpers +namespace Window { +class SessionController; +} // namespace Window + namespace Ui { class RpWidget; class GenericBox; @@ -38,14 +42,20 @@ void AddLevelBadge( const QMargins &padding, rpl::producer text); +enum class PeerColorTab { + Profile, + Name, +}; + void EditPeerColorBox( not_null box, - std::shared_ptr show, + not_null controller, not_null peer, std::shared_ptr style = nullptr, - std::shared_ptr theme = nullptr); + std::shared_ptr theme = nullptr, + PeerColorTab initialTab = PeerColorTab::Profile); -void AddPeerColorButton( +not_null AddPeerColorButton( not_null container, std::shared_ptr show, not_null peer, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index bd5210b5d8..c075dcdf90 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -1201,7 +1201,6 @@ void Controller::refreshForumToggleLocked() { void Controller::fillColorIndexButton() { Expects(_controls.buttonsLayout != nullptr); - const auto show = _navigation->uiShow(); AddPeerColorButton( _controls.buttonsLayout, _navigation->uiShow(), @@ -2451,6 +2450,8 @@ void Controller::saveTitle() { _controls.title->showError(); if (type == u"NO_CHAT_TITLE"_q) { _box->scrollToWidget(_controls.title); + } else { + _navigation->showToast(type); } cancelSave(); }; @@ -2529,8 +2530,9 @@ void Controller::saveDescription() { MTPstring() // Description. )).done([=] { successCallback(); - }).fail([=] { + }).fail([=](const MTP::Error &error) { _controls.description->showError(); + _navigation->showToast(error.type()); cancelSave(); }).send(); }).fail([=] { @@ -2550,6 +2552,7 @@ void Controller::saveDescription() { return; } _controls.description->showError(); + _navigation->showToast(type); cancelSave(); }).send(); } @@ -2621,9 +2624,11 @@ void Controller::togglePreHistoryHidden( channel->session().api().applyUpdates(result); apply(); }).fail([=](const MTP::Error &error) { - if (error.type() == u"CHAT_NOT_MODIFIED"_q) { + const auto type = error.type(); + if (type == u"CHAT_NOT_MODIFIED"_q) { apply(); } else { + _navigation->showToast(type); fail(); } }).send(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index cf6303ea70..eacaa448ca 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -173,7 +173,7 @@ private: link.usageLimit) : tr::lng_group_invite_no_joined(tr::now); const auto add = [&](const QString &text) { - result += QString::fromUtf8(" \xE2\x80\xA2 ") + text; + result += ' ' + Ui::kQBullet + ' ' + text; }; if (revoked) { return result; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 9d0e175db5..f88a81b59a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -714,6 +714,8 @@ template return checkView; }; + auto highlightWidget = QPointer(); + const auto highlightFlags = descriptor.highlightFlags; for (const auto &nestedWithLabel : descriptor.labels) { Assert(!nestedWithLabel.nested.empty()); @@ -725,16 +727,18 @@ template : object_ptr>{ nullptr }; const auto verticalLayout = wrap ? wrap->entity() : container.get(); auto innerChecks = std::vector>(); + auto sectionFlags = Flags(); for (const auto &entry : nestedWithLabel.nested) { const auto c = addCheckbox(verticalLayout, isInner, entry); if (isInner) { innerChecks.push_back(c); + sectionFlags |= entry.flags; } } if (wrap) { const auto raw = wrap.data(); raw->hide(anim::type::instant); - AddInnerToggle( + const auto toggle = AddInnerToggle( container, st, innerChecks, @@ -742,6 +746,9 @@ template *nestedWithLabel.nestingLabel, std::nullopt, { nestedWithLabel.nested.front().icon }); + if (highlightFlags && (sectionFlags & highlightFlags)) { + highlightWidget = toggle; + } container->add(std::move(wrap)); container->widthValue( ) | rpl::on_next([=](int w) { @@ -756,9 +763,10 @@ template } return { - nullptr, - value, - state->anyChanges.events() | rpl::map(value) + .widget = nullptr, + .value = value, + .changes = state->anyChanges.events() | rpl::map(value), + .highlightWidget = highlightWidget, }; } @@ -1145,7 +1153,7 @@ void ShowEditPeerPermissionsBox( Ui::AddSubsectionTitle( inner, tr::lng_rights_default_restrictions_header()); - auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions( + auto [checkboxes, getRestrictions, changes, highlightWidget] = CreateEditRestrictions( inner, restrictions, disabledMessages, @@ -1312,7 +1320,7 @@ Fn AboutGigagroupCallback( box->setTitle(tr::lng_gigagroup_convert_title()); const auto addFeature = [&](rpl::producer text) { using namespace rpl::mappers; - const auto prefix = QString::fromUtf8("\xE2\x80\xA2 "); + const auto prefix = Ui::kQBullet + ' '; box->addRow( object_ptr( box, @@ -1463,10 +1471,12 @@ ChatAdminRights AdminRightsForOwnershipTransfer( EditFlagsControl CreateEditPowerSaving( QWidget *parent, PowerSaving::Flags flags, - rpl::producer forceDisabledMessage) { + rpl::producer forceDisabledMessage, + PowerSaving::Flags highlightFlags) { auto widget = object_ptr(parent); auto descriptor = Settings::PowerSavingLabels(); descriptor.forceDisabledMessage = std::move(forceDisabledMessage); + descriptor.highlightFlags = highlightFlags; auto result = CreateEditFlags( widget.data(), flags, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h index 018b5523dc..4d7ed413c7 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h @@ -69,6 +69,7 @@ struct EditFlagsControl { object_ptr widget; Fn value; rpl::producer changes; + QPointer highlightWidget; }; template @@ -83,6 +84,7 @@ struct EditFlagsDescriptor { base::flat_map disabledMessages; const style::SettingsButton *st = nullptr; rpl::producer forceDisabledMessage; + Flags highlightFlags = Flags(); }; using RestrictionLabel = EditFlagsLabel; @@ -117,7 +119,8 @@ using AdminRightLabel = EditFlagsLabel; [[nodiscard]] auto CreateEditPowerSaving( QWidget *parent, PowerSaving::Flags flags, - rpl::producer forceDisabledMessage + rpl::producer forceDisabledMessage, + PowerSaving::Flags highlightFlags = PowerSaving::Flags() ) -> EditFlagsControl; [[nodiscard]] auto CreateEditAdminLogFilter( diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 592e225f83..8ef319377c 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -593,10 +593,25 @@ object_ptr CreateUserpicsTransfer( const auto raw = result.data(); const auto right = CreateChild(raw, to, st->button); const auto overlay = CreateChild(raw); + const auto drawCornerPeer = (type == Type::ChannelFutureOwner) + ? [&]() -> PaintRoundImageCallback { + using Peers = std::vector>; + const auto snapshot = rpl::variable( + rpl::duplicate(from)).current(); + if (snapshot.size() == 2) { + return ForceRoundUserpicCallback(snapshot[1].get()); + } + return nullptr; + }() + : (PaintRoundImageCallback)(nullptr); const auto state = raw->lifetime().make_state(); - std::move( - from + ((type == Type::ChannelFutureOwner) + ? std::move(from) | rpl::map([=]( + const std::vector> &list) { + return std::vector>{ list.front() }; + }) + : std::move(from) ) | rpl::on_next([=]( const std::vector> &list) { auto was = base::take(state->from); @@ -662,7 +677,7 @@ object_ptr CreateUserpicsTransfer( } state->layer.fill(Qt::transparent); - auto q = QPainter(&state->layer); + auto q = Painter(&state->layer); auto hq = PainterHighQualityEnabler(q); const auto stroke = st->stroke; const auto half = stroke / 2.; @@ -691,13 +706,24 @@ object_ptr CreateUserpicsTransfer( const auto x = back->x() + back->width() - w + add.x(); const auto y = back->y() + back->height() - h + add.y(); - auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y)); - brush.setStops(Ui::Premium::ButtonGradientStops()); - q.setBrush(brush); - pen.setWidthF(stroke); + pen.setWidthF(drawCornerPeer ? stroke * 2 : stroke); q.setPen(pen); q.drawEllipse(x - half, y - half, w + stroke, h + stroke); - icon.paint(q, x + skip, y + skip, outerw); + if (drawCornerPeer) { + drawCornerPeer( + q, + x - half, + y - half, + w + stroke, + w + stroke); + } else { + auto brush = QLinearGradient( + QPointF(x + w, y + h), + QPointF(x, y)); + brush.setStops(Ui::Premium::ButtonGradientStops()); + q.setBrush(brush); + icon.paint(q, x + skip, y + skip, outerw); + } } const auto size = st::boostReplaceArrow.size(); st::boostReplaceArrow.paint( @@ -952,6 +978,64 @@ private: }; +[[nodiscard]] PaintRoundImageCallback GenerateGiftUniqueUserpicCallback( + not_null session, + std::shared_ptr unique, + Fn update) { + struct State { + QImage layer; + std::shared_ptr bg; + std::shared_ptr sticker; + }; + const auto state = std::make_shared(); + const auto repaint = [=] { + if (update) { + update(); + } + }; + state->bg = std::make_shared(session, unique); + state->bg->subscribeToUpdates(repaint); + const auto tag = Data::CustomEmojiSizeTag::Isolated; + state->sticker = session->data().customEmojiManager().create( + unique->model.document, + repaint, + tag); + + return [=](QPainter &p, int x, int y, int outerw, int size) { + const auto ideal = st::boostReplaceUserpic.photoSize; + const auto scale = size / float64(ideal); + const auto ratio = style::DevicePixelRatio(); + if (state->layer.size() != QSize(ideal, ideal) * ratio) { + state->layer = QImage( + QSize(ideal, ideal) * ratio, + QImage::Format_ARGB32_Premultiplied); + state->layer.setDevicePixelRatio(ratio); + } + state->layer.fill(Qt::transparent); + + auto q = QPainter(&state->layer); + auto hq = PainterHighQualityEnabler(q); + const auto esize = Data::FrameSizeFromTag(tag) / ratio; + q.drawImage(QRect(0, 0, ideal, ideal), state->bg->image(ideal)); + state->sticker->paint(q, { + .textColor = st::windowFg->c, + .now = crl::now(), + .position = QPoint((ideal - esize) / 2, (ideal - esize) / 2), + }); + q.end(); + + if (scale != 1.) { + p.save(); + p.translate(x, y); + p.scale(scale, scale); + p.drawImage(0, 0, state->layer); + p.restore(); + } else { + p.drawImage(x, y, state->layer); + } + }; +} + object_ptr CreateGiftTransfer( not_null parent, std::shared_ptr unique, @@ -959,8 +1043,7 @@ object_ptr CreateGiftTransfer( struct State { QImage layer; QPoint giftPosition; - std::shared_ptr bg; - std::shared_ptr sticker; + PaintRoundImageCallback paintGift; }; const auto st = &st::boostReplaceUserpicsRow; const auto full = st->button.size.height() @@ -972,18 +1055,10 @@ object_ptr CreateGiftTransfer( const auto overlay = CreateChild(raw); const auto state = raw->lifetime().make_state(); - state->bg = std::make_shared( + state->paintGift = GenerateGiftUniqueUserpicCallback( &to->session(), - unique); - state->bg->subscribeToUpdates([=] { - overlay->update(); - }); - const auto tag = Data::CustomEmojiSizeTag::Isolated; - state->sticker = to->owner().customEmojiManager().create( - unique->model.document, - [=] { overlay->update(); }, - tag); - overlay->update(); + unique, + [=] { raw->update(); }); raw->widthValue( ) | rpl::on_next([=](int width) { @@ -1008,18 +1083,14 @@ object_ptr CreateGiftTransfer( } state->layer.fill(Qt::transparent); - auto q = QPainter(&state->layer); + auto q = Painter(&state->layer); auto hq = PainterHighQualityEnabler(q); - const auto from = QRect(state->giftPosition, right->size()); - const auto esize = Data::FrameSizeFromTag(tag) / ratio; - q.drawImage(from, state->bg->image(from.width())); - state->sticker->paint(q, { - .textColor = st::windowFg->c, - .now = crl::now(), - .position = from.topLeft() + QPoint( - (from.width() - esize) / 2, - (from.height() - esize) / 2), - }); + state->paintGift( + q, + state->giftPosition.x(), + state->giftPosition.y(), + outerw, + right->width()); const auto size = st::boostReplaceArrow.size(); st::boostReplaceArrow.paint( @@ -1037,3 +1108,4 @@ object_ptr CreateGiftTransfer( }, overlay->lifetime()); return result; } + diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h index 1ad668ac81..a505799c89 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -65,6 +65,7 @@ enum class UserpicsTransferType { BoostReplace, StarRefJoin, AuctionRecipient, + ChannelFutureOwner, }; [[nodiscard]] object_ptr CreateUserpicsTransfer( not_null parent, @@ -82,3 +83,15 @@ enum class UserpicsTransferType { not_null parent, std::shared_ptr unique, not_null to); + +using PaintRoundImageCallback = Fn; + +[[nodiscard]] PaintRoundImageCallback GenerateGiftUniqueUserpicCallback( + not_null session, + std::shared_ptr unique, + Fn update); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 5c71c72384..b50ac0c60c 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -30,7 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_premium_limits.h" #include "lang/lang_keys.h" -#include "settings/settings_premium.h" // ShowPremium. +#include "settings/sections/settings_premium.h" // ShowPremium. #include "base/unixtime.h" #include "apiwrap.h" #include "styles/style_premium.h" diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 0ba8e7e91a..fe23e31a6d 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" // kMaxAccounts #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" +#include "ui/controls/feature_list.h" #include "ui/layers/generic_box.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_graphics.h" @@ -34,8 +35,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/painter.h" #include "ui/vertical_list.h" -#include "settings/settings_business.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_business.h" +#include "settings/sections/settings_premium.h" #include "lottie/lottie_single_player.h" #include "history/view/media/history_view_sticker.h" #include "history/view/history_view_element.h" @@ -904,42 +905,28 @@ struct VideoPreviewDocument { } void AddGiftsInfoRows(not_null container) { - const auto infoRow = [&]( - rpl::producer title, - rpl::producer text, - not_null icon) { - auto raw = container->add( - object_ptr(container)); - raw->add( - object_ptr( - raw, - std::move(title) | rpl::map(tr::bold), - st::defaultFlatLabel), - st::settingsPremiumRowTitlePadding); - raw->add( - object_ptr( - raw, - std::move(text), - st::upgradeGiftSubtext), - st::settingsPremiumRowAboutPadding); - object_ptr( - raw, - *icon, - st::starrefInfoIconPosition); + const auto features = std::vector{ + { + st::menuIconUnique, + tr::lng_gift_upgrade_unique_title(tr::now), + tr::lng_gift_upgrade_unique_about(tr::now, tr::marked), + }, + { + st::menuIconTradable, + tr::lng_gift_upgrade_tradable_title(tr::now), + tr::lng_gift_upgrade_tradable_about(tr::now, tr::marked), + }, + { + st::menuIconNftWear, + tr::lng_gift_upgrade_wearable_title(tr::now), + tr::lng_gift_upgrade_wearable_about(tr::now, tr::marked), + }, }; - - infoRow( - tr::lng_gift_upgrade_unique_title(), - tr::lng_gift_upgrade_unique_about(), - &st::menuIconUnique); - infoRow( - tr::lng_gift_upgrade_tradable_title(), - tr::lng_gift_upgrade_tradable_about(), - &st::menuIconTradable); - infoRow( - tr::lng_gift_upgrade_wearable_title(), - tr::lng_gift_upgrade_wearable_about(), - &st::menuIconNftWear); + for (const auto &feature : features) { + container->add( + Ui::MakeFeatureListEntry(container, feature), + st::boxRowPadding); + } } void PreviewBox( diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 13710e41b7..75da06465b 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -22,7 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "boxes/premium_preview_box.h" #include "main/main_session.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/scroll_content_shadow.h" diff --git a/Telegram/SourceFiles/boxes/select_future_owner_box.cpp b/Telegram/SourceFiles/boxes/select_future_owner_box.cpp new file mode 100644 index 0000000000..e040b20cf1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/select_future_owner_box.cpp @@ -0,0 +1,439 @@ +/* +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/select_future_owner_box.h" + +#include "api/api_chat_participants.h" +#include "api/api_cloud_password.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/passcode_box.h" +#include "boxes/peer_list_controllers.h" +#include "boxes/peer_lists_box.h" +#include "boxes/peers/replace_boost_box.h" // CreateUserpicsTransfer. +#include "core/application.h" +#include "core/core_cloud_password.h" +#include "data/data_channel.h" +#include "dialogs/ui/chat_search_empty.h" +#include "boxes/peers/channel_ownership_transfer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +enum class ParticipantType { + Admins, + Members +}; + +class ParticipantsController : public PeerListController { +public: + ParticipantsController( + not_null window, + not_null channel, + ParticipantType type); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void loadMoreRows() override; + void itemDeselectedHook(not_null peer) override; + + void setOnRowClicked(Fn callback); + rpl::producer<> itemDeselected() const; + +private: + const not_null _window; + const not_null _channel; + const ParticipantType _type; + MTP::Sender _api; + Fn _onRowClicked; + rpl::event_stream<> _itemDeselected; + + mtpRequestId _loadRequestId = 0; + int _offset = 0; + bool _allLoaded = false; +}; + +ParticipantsController::ParticipantsController( + not_null window, + not_null 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 callback) { + _onRowClicked = callback; +} + +void ParticipantsController::prepare() { + loadMoreRows(); +} + +void ParticipantsController::rowClicked(not_null row) { + delegate()->peerListSetRowChecked( + row, + !delegate()->peerListIsRowChecked(row)); + for (auto i = 0; i < delegate()->peerListFullRowsCount(); ++i) { + auto r = delegate()->peerListRowAt(i); + if (r != row) { + delegate()->peerListSetRowChecked(r, false); + } + } + if (_onRowClicked) { + _onRowClicked(); + } +} + +void ParticipantsController::itemDeselectedHook(not_null peer) { + _itemDeselected.fire({}); +} + +rpl::producer<> ParticipantsController::itemDeselected() const { + return _itemDeselected.events(); +} + +void ParticipantsController::loadMoreRows() { + if (_loadRequestId || _allLoaded) { + return; + } + + const auto perPage = (_offset > 0) ? 200 : 50; + const auto participantsHash = uint64(0); + const auto filter = (_type == ParticipantType::Admins) + ? MTP_channelParticipantsAdmins() + : MTP_channelParticipantsRecent(); + + _loadRequestId = _api.request(MTPchannels_GetParticipants( + _channel->inputChannel(), + filter, + MTP_int(_offset), + MTP_int(perPage), + MTP_long(participantsHash) + )).done([=](const MTPchannels_ChannelParticipants &result) { + _loadRequestId = 0; + auto added = false; + + result.match([&](const MTPDchannels_channelParticipants &data) { + const auto &[availableCount, list] = Api::ChatParticipants::Parse( + _channel, + data); + for (const auto &participant : list) { + if (const auto user = _channel->owner().userLoaded( + participant.userId())) { + if (delegate()->peerListFindRow(user->id.value)) { + continue; + } + if (user->isBot()) { + continue; + } + using Type = Api::ChatParticipant::Type; + if ((participant.type() == Type::Creator) + || (_type == ParticipantType::Members + && (participant.type() == Type::Admin))) { + continue; + } + auto row = std::make_unique(user); + const auto promotedSince = participant.promotedSince(); + row->setCustomStatus( + (promotedSince + ? tr::lng_select_next_owner_box_status_promoted + : tr::lng_select_next_owner_box_status_joined)( + tr::now, + lt_date, + Ui::FormatDateTime( + base::unixtime::parse(promotedSince + ? promotedSince + : participant.memberSince())))); + delegate()->peerListAppendRow(std::move(row)); + added = true; + } + } + if (const auto size = list.size()) { + _offset += size; + } else { + _allLoaded = true; + } + }, [](const MTPDchannels_channelParticipantsNotModified &) { + }); + + if (!added && _offset > 0) { + _allLoaded = true; + } + delegate()->peerListRefreshRows(); + }).fail([=] { + _loadRequestId = 0; + }).send(); +} + +} // namespace + +void SelectFutureOwnerbox( + not_null box, + not_null channel, + not_null user) { + const auto content = box->verticalLayout(); + Ui::AddSkip(content); + Ui::AddSkip(content); + content->add( + CreateUserpicsTransfer( + content, + rpl::single(std::vector>{ + user->session().user(), + channel, + }), + user, + UserpicsTransferType::ChannelFutureOwner), + st::boxRowPadding); + Ui::AddSkip(content); + Ui::AddSkip(content); + content->add( + object_ptr( + content, + channel->isMegagroup() + ? 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); + content->add( + object_ptr( + content, + (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), + tr::rich), + st::boxLabel), + st::boxRowPadding); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto select = content->add( + object_ptr( + content, + !adminsAreEqual + ? tr::lng_select_next_owner_box() + : tr::lng_select_next_owner_box_admin(), + st::defaultLightButton), + st::boxRowPadding, + style::al_justify); + Ui::AddSkip(content); + const auto cancel = content->add( + object_ptr( + content, + tr::lng_cancel(), + st::defaultLightButton), + st::boxRowPadding, + style::al_justify); + cancel->setClickedCallback([=] { + box->closeBox(); + }); + Ui::AddSkip(content); + const auto leave = content->add( + object_ptr( + content, + channel->isMegagroup() + ? 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); + box->closeBox(); + }); + select->setClickedCallback([=] { + const auto window = Core::App().findWindow(box); + const auto sessionController = window + ? window->sessionController() + : nullptr; + if (!sessionController) { + return; + } + + auto adminsOwned = std::make_unique( + sessionController, + channel, + ParticipantType::Admins); + + auto membersOwned = std::make_unique( + sessionController, + channel, + ParticipantType::Members); + + const auto admins = adminsOwned.get(); + const auto members = membersOwned.get(); + + auto initBox = [=](not_null selectBox) { + struct State { + base::unique_qptr noLists; + rpl::event_stream<> selectionChanges; + }; + const auto state = selectBox->lifetime().make_state(); + const auto uncheckOtherList = [=]( + not_null otherController) { + auto delegate = otherController->delegate(); + const auto full = delegate->peerListFullRowsCount(); + for (auto i = 0; i < full; ++i) { + delegate->peerListSetRowChecked( + delegate->peerListRowAt(i), + false); + } + state->selectionChanges.fire({}); + }; + admins->setOnRowClicked([=] { uncheckOtherList(members); }); + members->setOnRowClicked([=] { uncheckOtherList(admins); }); + selectBox->setTitle(!adminsAreEqual + ? tr::lng_select_next_owner_box_title() + : tr::lng_select_next_owner_box_title_admin()); + const auto searchEnabled = PeerListSearchMode::Enabled; + admins->delegate()->peerListSetSearchMode(searchEnabled); + members->delegate()->peerListSetSearchMode(searchEnabled); + rpl::merge( + admins->itemDeselected(), + members->itemDeselected() + ) | rpl::on_next([=] { + state->selectionChanges.fire({}); + }, selectBox->lifetime()); + const auto separatorAdmins = selectBox->addSeparatorBefore( + 0, + CreatePeerListSectionSubtitle( + selectBox, + !channel->isMegagroup() + ? 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() + ? tr::lng_select_next_owner_box_sub_members() + : tr::lng_select_next_owner_box_sub_members_group())); + rpl::combine( + separatorAdmins->heightValue(), + separatorMembers->heightValue() + ) | rpl::map( + (rpl::mappers::_1 + rpl::mappers::_2) > 0 + ) | rpl::distinct_until_changed() | rpl::on_next([=](bool has) { + qDebug() << "has" << has; + if (has) { + state->noLists = nullptr; + return; + } + using namespace Dialogs; + state->noLists = base::make_unique_q( + selectBox, + SearchEmpty::Icon::NoResults, + (adminsAreEqual + ? tr::lng_select_next_owner_box_empty_list + : tr::lng_select_next_owner_box_empty_list_admin)( + tr::rich)); + state->noLists->show(); + state->noLists->raise(); + selectBox->sizeValue( + ) | rpl::filter_size() | rpl::on_next([=](QSize s) { + state->noLists->setMinimalHeight(s.height() / 3); + state->noLists->resizeToWidth(s.width() / 3 * 2); + state->noLists->moveToLeft( + (s.width() - state->noLists->width()) / 2, + (s.height() - state->noLists->height()) / 2); + }, state->noLists->lifetime()); + crl::on_main(state->noLists.get(), [=] { + state->noLists->animate(); + }); + }, selectBox->lifetime()); + { + const auto &st = st::futureOwnerBoxSelect; + selectBox->setStyle(st); + auto button = object_ptr( + selectBox, + rpl::conditional( + state->selectionChanges.events( + ) | rpl::map([=] { + return !selectBox->collectSelectedRows().empty(); + }), + tr::lng_select_next_owner_box_confirm(), + tr::lng_close()), + st::defaultActiveButton); + button->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + const auto raw = button.data(); + rpl::combine( + state->selectionChanges.events() | rpl::map_to(0), + selectBox->widthValue() + ) | rpl::on_next([=](int, int width) { + raw->resizeToWidth(width + - st.buttonPadding.left() + - st.buttonPadding.right()); + }, selectBox->lifetime()); + button->setFullRadius(true); + button->setClickedCallback([=] { + const auto selected = selectBox->collectSelectedRows(); + if (selected.empty()) { + return selectBox->closeBox(); + } + if (const auto user = selected.front()->asUser()) { + auto &lifetime = selectBox->lifetime(); + lifetime.make_state( + channel, + user, + selectBox->uiShow(), + [=](std::shared_ptr show) { + const auto revoke = false; + channel->session().api().deleteConversation( + channel, + revoke); + show->hideLayer(); + })->start(); + } + }); + selectBox->addButton(std::move(button)); + state->selectionChanges.fire({}); + } + }; + + auto controllers = std::vector>(); + controllers.reserve(2); + controllers.push_back(std::move(adminsOwned)); + controllers.push_back(std::move(membersOwned)); + box->uiShow()->showBox( + Box(std::move(controllers), initBox)); + }); + for (const auto &b : { select, cancel, leave }) { + b->setFullRadius(true); + b->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + } + box->setStyle(st::futureOwnerBox); +} \ No newline at end of file diff --git a/Telegram/SourceFiles/boxes/select_future_owner_box.h b/Telegram/SourceFiles/boxes/select_future_owner_box.h new file mode 100644 index 0000000000..7f48bf64c1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/select_future_owner_box.h @@ -0,0 +1,22 @@ +/* +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 + +class ChannelData; +class PeerData; +class UserData; + +namespace Ui { +class GenericBox; +class Show; +} // namespace Ui + +void SelectFutureOwnerbox( + not_null box, + not_null channel, + not_null user); diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index b4c9e5f450..b3b237c079 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "lang/lang_keys.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "payments/payments_checkout_process.h" #include "payments/payments_form.h" @@ -301,7 +302,7 @@ void AddTerms( void SendCreditsBox( not_null box, std::shared_ptr form, - Fn sent) { + Fn sent) { if (!form) { return; } @@ -391,7 +392,7 @@ void SendCreditsBox( Ui::AddSkip(content); Ui::AddSkip(content); - const auto button = box->addButton(rpl::single(QString()), [=] { + const auto sendStars = [=] { if (state->confirmButtonBusy.current()) { return; } @@ -411,7 +412,7 @@ void SendCreditsBox( state->confirmButtonBusy = false; box->closeBox(); } - sent(); + sent(Settings::SmallBalanceResult::Success); }).fail([=](const MTP::Error &error) { if (weak) { state->confirmButtonBusy = false; @@ -433,6 +434,22 @@ void SendCreditsBox( show->showToast(id); } }).send(); + }; + + const auto button = box->addButton(rpl::single(QString()), [=] { + Settings::MaybeRequestBalanceIncrease( + Main::MakeSessionShow(box->uiShow(), session), + form->invoice.credits, + SmallBalanceSourceFromForm(form), + [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Cancelled) { + } else if (result == Settings::SmallBalanceResult::Success + || result == Settings::SmallBalanceResult::Already) { + sendStars(); + } else { + sent(result); + } + }); }); if (form->invoice.subscriptionPeriod) { AddTerms(box, button, stBox); @@ -573,4 +590,16 @@ void SendStarsForm( }).send(); } +Settings::SmallBalanceSource SmallBalanceSourceFromForm( + std::shared_ptr form) { + using namespace Payments; + using namespace Settings; + const auto starGift = std::get_if(&form->id.value); + return !starGift + ? SmallBalanceSource(SmallBalanceBot{ .botId = form->botId }) + : SmallBalanceSource(SmallBalanceStarGift{ + .recipientId = starGift->recipient->id, + }); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h index 43fb75b033..d2bef3e54f 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.h +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -21,6 +21,11 @@ namespace Payments { struct CreditsFormData; } // namespace Payments +namespace Settings { +enum class SmallBalanceResult; +struct SmallBalanceSource; +} // namespace Settings + namespace Ui { class RpWidget; @@ -30,7 +35,7 @@ class FlatLabel; void SendCreditsBox( not_null box, std::shared_ptr data, - Fn sent); + Fn sent); [[nodiscard]] TextWithEntities CreditsEmoji(); @@ -55,4 +60,7 @@ void SendStarsForm( std::shared_ptr data, Fn)> done); +[[nodiscard]] Settings::SmallBalanceSource SmallBalanceSourceFromForm( + std::shared_ptr form); + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 7d6a151c5f..0a2f9b84bf 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -276,14 +276,16 @@ SendFilesBox::Block::Block( }); _preview.reset(preview); } else { - const auto media = Ui::SingleMediaPreview::Create( - parent, - st, - gifPaused, - first, - [=](Ui::AttachActionType type) { - return actionAllowed((*_items)[from], type); - }); + const auto media = way.sendImagesAsPhotos() + ? Ui::SingleMediaPreview::Create( + parent, + st, + gifPaused, + first, + [=](Ui::AttachActionType type) { + return actionAllowed((*_items)[from], type); + }) + : nullptr; if (media) { _isSingleMedia = true; _preview.reset(media); @@ -652,7 +654,9 @@ void SendFilesBox::setupDragArea() { auto computeState = [=](const QMimeData *data) { using DragState = Storage::MimeDataState; const auto state = Storage::ComputeMimeDataState(data); - return (state == DragState::PhotoFiles || state == DragState::Image) + return (state == DragState::PhotoFiles + || state == DragState::Image + || state == DragState::MediaFiles) ? (_sendWay.current().sendImagesAsPhotos() ? DragState::Image : DragState::Files) @@ -1022,11 +1026,11 @@ void SendFilesBox::initSendWay() { const auto was = hidden(); updateCaptionPlaceholder(); updateEmojiPanelGeometry(); - for (auto &block : _blocks) { - block.setSendWay(value); - } - refreshButtons(); - refreshPriceTag(); + applyBlockChanges(); + generatePreviewFrom(0); + _inner->resizeToWidth(st::boxWideWidth); + refreshControls(); + captionResized(); if (was != hidden()) { updateBoxSize(); updateControlsGeometry(); @@ -1349,15 +1353,15 @@ void SendFilesBox::setupSendWayControls() { _st.files.check); _sendImagesAsPhotos.create( this, - tr::lng_send_compressed(tr::now), - _sendWay.current().sendImagesAsPhotos(), + tr::lng_send_as_documents(tr::now), + !_sendWay.current().sendImagesAsPhotos(), _st.files.checkbox, _st.files.check); _sendWay.changes( ) | rpl::on_next([=](SendFilesWay value) { _groupFiles->setChecked(value.groupFiles()); - _sendImagesAsPhotos->setChecked(value.sendImagesAsPhotos()); + _sendImagesAsPhotos->setChecked(!value.sendImagesAsPhotos()); }, lifetime()); _groupFiles->checkedChanges( @@ -1379,10 +1383,10 @@ void SendFilesBox::setupSendWayControls() { _sendImagesAsPhotos->checkedChanges( ) | rpl::on_next([=](bool checked) { auto sendWay = _sendWay.current(); - if (sendWay.sendImagesAsPhotos() == checked) { + if (sendWay.sendImagesAsPhotos() == !checked) { return; } - sendWay.setSendImagesAsPhotos(checked); + sendWay.setSendImagesAsPhotos(!checked); if (checkWithWay(sendWay)) { _sendWay = sendWay; } else { @@ -1402,9 +1406,10 @@ void SendFilesBox::setupSendWayControls() { rpl::combine( _groupFiles->checkedValue(), _sendImagesAsPhotos->checkedValue() - ) | rpl::on_next([=](bool groupFiles, bool asPhoto) { + ) | rpl::on_next([=](bool groupFiles, bool asDocuments) { _wayRemember->setVisible( - (groupFiles != groupFilesFirst) || (asPhoto != asPhotosFirst)); + (groupFiles != groupFilesFirst) + || ((!asDocuments) != asPhotosFirst)); captionResized(); }, lifetime()); @@ -1441,8 +1446,8 @@ void SendFilesBox::updateSendWayControls() { _sendImagesAsPhotos->setVisible( _list.hasSendImagesAsPhotosOption(onlyOne)); _sendImagesAsPhotos->setText((_list.files.size() > 1) - ? tr::lng_send_compressed(tr::now) - : tr::lng_send_compressed_one(tr::now)); + ? tr::lng_send_as_documents(tr::now) + : tr::lng_send_as_documents_one(tr::now)); _hintLabel->setVisible( _show->session().settings().photoEditorHintShown() diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index e6ea5a3699..16822de42f 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -33,7 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/view/history_view_element.h" #include "history/view/history_view_context_menu.h" // CopyPostLink. -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/window_session_controller.h" #include "boxes/peer_list_controllers.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -1319,6 +1319,12 @@ void ShareBox::Inner::updateUpon(const QPoint &pos) { auto x = pos.x(), y = pos.y(); auto row = (y - _rowsTop) / _rowHeight; auto column = qFloor((x - _rowsLeft) / _rowWidthReal); + + if (column < 0 || column >= _columnCount) { + _upon = -1; + return; + } + auto left = _rowsLeft + qFloor(column * _rowWidthReal) + st::shareColumnSkip / 2; auto top = _rowsTop + row * _rowHeight + st::sharePhotoTop; auto xupon = (x >= left) && (x < left + (_rowWidth - st::shareColumnSkip)); diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index 29960cb216..af11e45a23 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -952,7 +952,7 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { lt_gift, tr::bold(name), tr::marked), - helper.context()); + helper.context()).widget; } [[nodiscard]] object_ptr AuctionInfoTable( @@ -1193,7 +1193,7 @@ void AuctionGotGiftsBox( ).append(' ').append( helper.paletteDependent( Text::CustomEmojiTextBadge( - '#' + QString::number(entry.position), + '#' + Lang::FormatCountDecimal(entry.position), st::defaultTableSmallButton))); AddTableRow( table, @@ -1304,7 +1304,7 @@ void AuctionInfoBox( struct State { explicit State(not_null session) - : delegate(session, GiftButtonMode::Minimal) { + : delegate(session, GiftButtonMode::Minimal) { } Delegate delegate; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 5e1d66f77a..1708b25725 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -7,12 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/star_gift_box.h" +#include "boxes/star_gift_cover_box.h" + #include "apiwrap.h" #include "api/api_credits.h" #include "api/api_global_privacy.h" #include "api/api_premium.h" #include "api/api_text_entities.h" -//#include "base/call_delayed.h" #include "base/event_filter.h" #include "base/qt_signal_producer.h" #include "base/random.h" @@ -74,9 +75,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_form.h" #include "payments/payments_checkout_process.h" #include "payments/payments_non_panel_process.h" -#include "settings/settings_credits.h" +#include "settings/sections/settings_credits.h" #include "settings/settings_credits_graphics.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/boxes/about_cocoon_box.h" // Ui::AddUniqueCloseButton #include "ui/boxes/boost_box.h" #include "ui/boxes/confirm_box.h" @@ -85,6 +86,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/controls/button_labels.h" #include "ui/controls/emoji_button.h" +#include "ui/controls/feature_list.h" #include "ui/controls/ton_common.h" #include "ui/controls/userpic_button.h" #include "ui/effects/path_shift_gradient.h" @@ -139,32 +141,15 @@ constexpr auto kPriceTabCollectibles = -2; constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); -constexpr auto kCrossfadeDuration = crl::time(400); constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); -constexpr auto kGradientButtonBgOpacity = 0.6; - -constexpr auto kSpinnerBackdrops = 6; -constexpr auto kSpinnerPatterns = 6; -constexpr auto kSpinnerModels = 6; - -constexpr auto kBackdropSpinDuration = crl::time(300); -constexpr auto kBackdropStopsAt = crl::time(2.5 * 1000); -constexpr auto kPatternSpinDuration = crl::time(600); -constexpr auto kPatternStopsAt = crl::time(4 * 1000); -constexpr auto kModelSpinDuration = crl::time(160); -constexpr auto kModelStopsAt = crl::time(5.5 * 1000); -constexpr auto kModelScaleFrom = 0.7; using namespace HistoryView; using namespace Info::PeerGifts; using Data::GiftAttributeId; using Data::GiftAttributeIdType; -using Data::ResaleGiftsSort; -using Data::ResaleGiftsFilter; -using Data::ResaleGiftsDescriptor; using Data::MyGiftsDescriptor; enum class PickType { @@ -496,7 +481,9 @@ auto GenerateGiftMedia( tr::bold); }, [&](const GiftTypeStars &gift) { return recipient->isSelf() - ? tr::lng_action_gift_self_subtitle(tr::now, tr::bold) + ? ((gift.info.unique && gift.info.unique->crafted) + ? tr::lng_action_gift_crafted_subtitle(tr::now, tr::bold) + : tr::lng_action_gift_self_subtitle(tr::now, tr::bold)) : tr::lng_action_gift_got_subtitle( tr::now, lt_user, @@ -1288,59 +1275,6 @@ void SendGift( }); } -[[nodiscard]] std::shared_ptr FindUniqueGift( - not_null session, - const MTPUpdates &updates) { - auto result = std::shared_ptr(); - const auto checkAction = [&](const MTPDmessageService &message) { - const auto &action = message.vaction(); - action.match([&](const MTPDmessageActionStarGiftUnique &data) { - if (const auto gift = Api::FromTL(session, data.vgift())) { - const auto to = data.vpeer() - ? peerFromMTP(*data.vpeer()) - : PeerId(); - const auto service = data.vfrom_id() - && session->data().peer( - peerFromMTP(*data.vfrom_id()))->isServiceUser(); - const auto channel = (service && peerIsChannel(to)) - ? session->data().channel(peerToChannel(to)).get() - : nullptr; - const auto channelSavedId = channel - ? data.vsaved_id().value_or_empty() - : uint64(); - const auto realGiftMsgId = (peerIsUser(to) && data.vsaved_id()) - ? MsgId(data.vsaved_id().value_or_empty()) - : MsgId(message.vid().v); - - result = std::make_shared( - Data::GiftUpgradeResult{ - .info = *gift, - .manageId = (channel && channelSavedId) - ? Data::SavedStarGiftId::Chat( - channel, - channelSavedId) - : Data::SavedStarGiftId::User(realGiftMsgId), - .date = message.vdate().v, - .starsForDetailsRemove = int( - data.vdrop_original_details_stars( - ).value_or_empty()), - .saved = data.is_saved(), - }); - } - }, [](const auto &) {}); - }; - updates.match([&](const MTPDupdates &data) { - for (const auto &update : data.vupdates().v) { - update.match([&](const MTPDupdateNewMessage &data) { - data.vmessage().match([&](const MTPDmessageService &data) { - checkAction(data); - }, [](const auto &) {}); - }, [](const auto &) {}); - } - }, [](const auto &) {}); - return result; -} - void ShowGiftUpgradedToast( not_null window, not_null session, @@ -1660,310 +1594,6 @@ void CheckMaybeGiftLocked( })).send(); } -[[nodiscard]] object_ptr MakeGiftsList( - not_null window, - not_null peer, - rpl::producer gifts, - Fn loadMore) { - auto result = object_ptr((QWidget*)nullptr); - const auto raw = result.data(); - - Data::AmPremiumValue(&window->session()) | rpl::on_next([=] { - raw->update(); - }, raw->lifetime()); - - struct State { - Delegate delegate; - std::vector order; - std::vector validated; - std::vector list; - std::vector> buttons; - std::shared_ptr api; - std::shared_ptr transferRequested; - rpl::variable visibleRange; - uint64 resaleRequestingId = 0; - rpl::lifetime resaleLifetime; - bool sending = false; - int perRow = 1; - }; - const auto state = raw->lifetime().make_state(State{ - .delegate = Delegate(&window->session(), GiftButtonMode::Full), - }); - const auto single = state->delegate.buttonSize(); - const auto shadow = st::defaultDropdownMenu.wrap.shadow; - const auto extend = shadow.extend; - - auto &packs = window->session().giftBoxStickersPacks(); - packs.updated() | rpl::on_next([=] { - for (const auto &button : state->buttons) { - if (const auto raw = button.get()) { - raw->update(); - } - } - }, raw->lifetime()); - - const auto rebuild = [=] { - const auto width = st::boxWideWidth; - const auto padding = st::giftBoxPadding; - const auto available = width - padding.left() - padding.right(); - const auto range = state->visibleRange.current(); - const auto count = int(state->list.size()); - - auto &buttons = state->buttons; - if (buttons.size() < count) { - buttons.resize(count); - } - auto &validated = state->validated; - validated.resize(count); - - auto x = padding.left(); - auto y = padding.top(); - const auto perRow = state->perRow; - const auto singlew = single.width() + st::giftBoxGiftSkip.x(); - const auto singleh = single.height() + st::giftBoxGiftSkip.y(); - const auto rowFrom = std::max(range.top - y, 0) / singleh; - const auto rowTill = (std::max(range.bottom - y + st::giftBoxGiftSkip.y(), 0) + singleh - 1) - / singleh; - Assert(rowTill >= rowFrom); - const auto first = rowFrom * perRow; - const auto last = std::min(rowTill * perRow, count); - auto checkedFrom = 0; - auto checkedTill = int(buttons.size()); - const auto ensureButton = [&](int index) { - auto &button = buttons[index]; - if (!button) { - validated[index] = false; - for (; checkedFrom != first; ++checkedFrom) { - if (buttons[checkedFrom]) { - button = std::move(buttons[checkedFrom]); - break; - } - } - } - if (!button) { - for (; checkedTill != last; ) { - --checkedTill; - if (buttons[checkedTill]) { - button = std::move(buttons[checkedTill]); - break; - } - } - } - if (!button) { - button = std::make_unique(raw, &state->delegate); - } - const auto raw = button.get(); - if (validated[index]) { - return; - } - raw->show(); - validated[index] = true; - const auto &descriptor = state->list[state->order[index]]; - raw->setDescriptor(descriptor, GiftButton::Mode::Full); - raw->setClickedCallback([=] { - const auto star = std::get_if(&descriptor); - const auto send = crl::guard(raw, [=] { - window->show(Box( - SendGiftBox, - window, - peer, - state->api, - descriptor, - nullptr)); - }); - const auto unique = star ? star->info.unique : nullptr; - const auto premiumNeeded = star && star->info.requirePremium; - if (unique && star->resale) { - window->show(Box( - Settings::GlobalStarGiftBox, - window->uiShow(), - star->info, - Settings::StarGiftResaleInfo{ - .recipientId = peer->id, - .forceTon = star->forceTon, - }, - Settings::CreditsEntryBoxStyleOverrides())); - } else if (unique && star->mine && !peer->isSelf()) { - if (ShowTransferGiftLater(window->uiShow(), unique)) { - return; - } - const auto done = [=] { - window->session().credits().load(true); - window->showPeerHistory(peer); - }; - if (state->transferRequested == unique) { - return; - } - state->transferRequested = unique; - const auto savedId = star->transferId; - using Payments::CheckoutResult; - const auto formReady = [=]( - uint64 formId, - CreditsAmount price, - std::optional failure) { - state->transferRequested = nullptr; - if (!failure && !price.stars()) { - LOG(("API Error: " - "Bad transfer invoice currenct.")); - } else if (!failure - || *failure == CheckoutResult::Free) { - unique->starsForTransfer = failure - ? 0 - : price.whole(); - ShowTransferToBox( - window, - peer, - unique, - savedId, - done); - } else if (*failure == CheckoutResult::Cancelled) { - done(); - } - }; - RequestOurForm( - window->uiShow(), - MTP_inputInvoiceStarGiftTransfer( - Api::InputSavedStarGiftId(savedId, unique), - peer->input()), - formReady); - } else if (star && star->resale) { - const auto id = star->info.id; - if (state->resaleRequestingId == id) { - return; - } - state->resaleRequestingId = id; - state->resaleLifetime = ShowStarGiftResale( - window, - peer, - id, - star->info.resellTitle, - [=] { state->resaleRequestingId = 0; }); - } else if (star && star->info.auction()) { - if (!IsSoldOut(star->info) - && premiumNeeded - && !peer->session().premium()) { - Settings::ShowPremiumGiftPremium(window, star->info); - } else { - const auto id = star->info.id; - if (state->resaleRequestingId == id) { - return; - } - state->resaleRequestingId = id; - state->resaleLifetime = ShowStarGiftAuction( - window, - peer, - id, - [=] { state->resaleRequestingId = 0; }, - crl::guard(raw, [=] { - state->resaleLifetime.destroy(); - })); - } - } else if (star && IsSoldOut(star->info)) { - window->show(Box(SoldOutBox, window, *star)); - } else if (premiumNeeded && !peer->session().premium()) { - Settings::ShowPremiumGiftPremium(window, star->info); - } else if (star - && star->info.lockedUntilDate - && star->info.lockedUntilDate > base::unixtime::now()) { - const auto ready = crl::guard(raw, [=] { - if (premiumNeeded && !peer->session().premium()) { - Settings::ShowPremiumGiftPremium( - window, - v::get(descriptor).info); - } else { - send(); - } - }); - CheckMaybeGiftLocked(window, star->info.id, ready); - } else if (star - && star->info.perUserTotal - && !star->info.perUserRemains) { - window->showToast({ - .text = tr::lng_gift_sent_finished( - tr::now, - lt_count, - star->info.perUserTotal, - tr::rich), - }); - } else { - send(); - } - }); - raw->setGeometry(QRect(QPoint(x, y), single), extend); - }; - y += rowFrom * singleh; - for (auto row = rowFrom; row != rowTill; ++row) { - for (auto col = 0; col != perRow; ++col) { - const auto index = row * perRow + col; - if (index >= count) { - break; - } - const auto last = !((col + 1) % perRow); - if (last) { - x = padding.left() + available - single.width(); - } - ensureButton(index); - if (last) { - x = padding.left(); - y += singleh; - } else { - x += singlew; - } - } - } - const auto till = std::min(int(buttons.size()), rowTill * perRow); - for (auto i = count; i < till; ++i) { - if (const auto button = buttons[i].get()) { - button->hide(); - } - } - - const auto page = range.bottom - range.top; - if (loadMore && page > 0 && range.bottom + page > raw->height()) { - loadMore(); - } - }; - - state->visibleRange = raw->visibleRange(); - state->visibleRange.value( - ) | rpl::on_next(rebuild, raw->lifetime()); - - std::move( - gifts - ) | rpl::on_next([=](const GiftsDescriptor &gifts) { - const auto width = st::boxWideWidth; - const auto padding = st::giftBoxPadding; - const auto available = width - padding.left() - padding.right(); - state->perRow = available / single.width(); - state->list = std::move(gifts.list); - state->api = gifts.api; - - const auto count = int(state->list.size()); - state->order = ranges::views::ints - | ranges::views::take(count) - | ranges::to_vector; - state->validated.clear(); - - if (SortForBirthday(peer)) { - ranges::stable_partition(state->order, [&](int i) { - const auto &gift = state->list[i]; - const auto stars = std::get_if(&gift); - return stars && stars->info.birthday && !stars->info.unique; - }); - } - - const auto rows = (count + state->perRow - 1) / state->perRow; - const auto height = padding.top() - + (rows * single.height()) - + ((rows - 1) * st::giftBoxGiftSkip.y()) - + padding.bottom(); - raw->resize(raw->width(), height); - rebuild(); - }, raw->lifetime()); - - return result; -} - void FillBg(not_null box) { box->paintRequest() | rpl::on_next([=] { auto p = QPainter(box); @@ -2018,13 +1648,18 @@ void AddBlock( state->gifts = GiftsPremium(&window->session(), peer); - auto result = MakeGiftsList(window, peer, state->gifts.value( + auto gifts = state->gifts.value( ) | rpl::map([=](const PremiumGiftsDescriptor &gifts) { return GiftsDescriptor{ gifts.list | ranges::to>, gifts.api, }; - }), nullptr); + }); + auto result = MakeGiftsList({ + .window = window, + .peer = peer, + .gifts = std::move(gifts), + }); result->lifetime().add([state = std::move(state)] {}); return result; } @@ -2057,7 +1692,8 @@ void AddBlock( tabSelected(tab); }, tabs.widget->lifetime()); result->add(std::move(tabs.widget)); - result->add(MakeGiftsList(window, peer, rpl::combine( + + auto gifts = rpl::combine( state->gifts.value(), state->priceTab.value(), rpl::single(rpl::empty) | rpl::then(state->myUpdated.events()) @@ -2103,7 +1739,8 @@ void AddBlock( return GiftsDescriptor{ gifts | ranges::to>(), }; - }), [=] { + }); + const auto loadMore = [=] { if (state->priceTab.current() == kPriceTabMy && !state->my.offset.isEmpty() && !state->myLoading) { @@ -2123,6 +1760,12 @@ void AddBlock( state->myUpdated.fire({}); }); } + }; + result->add(MakeGiftsList({ + .window = window, + .peer = peer, + .gifts = std::move(gifts), + .loadMore = loadMore, })); return result; @@ -2784,1068 +2427,6 @@ void ShowStarGiftBox( }, i->second.lifetime); } -void SetupResalePriceButton( - not_null parent, - rpl::producer background, - rpl::producer price, - Fn click) { - const auto resale = Ui::CreateChild< - Ui::FadeWrapScaled - >(parent, object_ptr(parent)); - resale->move(0, 0); - - const auto button = resale->entity(); - const auto text = Ui::CreateChild( - button, - QString(), - st::uniqueGiftResalePrice); - text->setAttribute(Qt::WA_TransparentForMouseEvents); - text->sizeValue() | rpl::on_next([=](QSize size) { - const auto padding = st::uniqueGiftResalePadding; - const auto margin = st::uniqueGiftResaleMargin; - button->resize(size.grownBy(padding + margin)); - text->move((margin + padding).left(), (margin + padding).top()); - }, button->lifetime()); - text->setTextColorOverride(QColor(255, 255, 255, 255)); - - std::move(price) | rpl::on_next([=](CreditsAmount value) { - if (value) { - text->setMarkedText(value.ton() - ? Ui::Text::IconEmoji(&st::tonIconEmoji).append( - Lang::FormatCreditsAmountDecimal(value)) - : Ui::Text::IconEmoji(&st::starIconEmoji).append( - Lang::FormatCountDecimal(value.whole()))); - resale->toggle(true, anim::type::normal); - } else { - resale->toggle(false, anim::type::normal); - } - }, resale->lifetime()); - resale->finishAnimating(); - - const auto bg = button->lifetime().make_state>( - std::move(background)); - button->paintRequest() | rpl::on_next([=] { - auto p = QPainter(button); - auto hq = PainterHighQualityEnabler(p); - - const auto inner = button->rect().marginsRemoved( - st::uniqueGiftResaleMargin); - const auto radius = inner.height() / 2.; - p.setPen(Qt::NoPen); - p.setBrush(bg->current()); - p.drawRoundedRect(inner, radius, radius); - }, button->lifetime()); - bg->changes() | rpl::on_next([=] { - button->update(); - }, button->lifetime()); - - if (click) { - resale->entity()->setClickedCallback(std::move(click)); - } else { - resale->setAttribute(Qt::WA_TransparentForMouseEvents); - } -} - -void AddUniqueGiftCover( - not_null container, - rpl::producer data, - UniqueGiftCoverArgs &&args) { - using SpinnerState = Data::GiftUpgradeSpinner::State; - - const auto cover = container->add(object_ptr(container)); - - struct Released { - Released() : link(QColor(255, 255, 255)) { - } - - rpl::variable subtitleText; - std::optional stars; - style::owned_color link; - style::FlatLabel st; - rpl::variable by; - base::unique_qptr subtitle; - base::unique_qptr subtitleButton; - rpl::variable subtitleHeight; - bool outlined = false; - QColor bg; - QColor fg; - }; - const auto released = cover->lifetime().make_state(); - - struct BackdropView { - Data::UniqueGiftBackdrop colors; - QImage gradient; - }; - struct PatternView { - DocumentData *document = nullptr; - std::unique_ptr emoji; - base::flat_map> emojis; - }; - struct ModelView { - std::shared_ptr media; - std::unique_ptr lottie; - rpl::lifetime lifetime; - }; - struct GiftView { - std::optional gift; - BackdropView backdrop; - PatternView pattern; - ModelView model; - bool forced = false; - }; - struct AttributeSpin { - AttributeSpin(crl::time duration) : duration(duration) { - } - - Animations::Simple animation; - crl::time duration = 0; - int wasIndex = -1; - int nowIndex = -1; - int willIndex = -1; - - [[nodiscard]] float64 progress() const { - return animation.value(1.); - } - void startWithin(int count, Fn update) { - Expects(count > 0); - - wasIndex = nowIndex; - nowIndex = willIndex; - willIndex = (willIndex < 0 ? 1 : (willIndex + 1)) % count; - animation.start(update, 0., 1., duration); - } - void startToTarget(Fn update, int slowdown = 1) { - if (willIndex != 0) { - wasIndex = nowIndex; - nowIndex = willIndex; - willIndex = 0; - animation.start( - update, - 0., - 1., - duration * 3 * slowdown, - anim::easeOutCubic); - } - } - }; - struct State { - std::shared_ptr spinner; - Fn checkSpinnerStart; - GiftView now; - GiftView next; - Animations::Simple crossfade; - Animations::Simple heightAnimation; - std::vector spinnerBackdrops; - std::vector spinnerPatterns; - std::vector spinnerModels; - AttributeSpin backdropSpin = kBackdropSpinDuration; - AttributeSpin patternSpin = kPatternSpinDuration; - AttributeSpin modelSpin = kModelSpinDuration; - crl::time spinStarted = 0; - int heightFinal = 0; - bool crossfading = false; - bool updateAttributesPending = false; - }; - const auto state = cover->lifetime().make_state(); - state->spinner = std::move(args.upgradeSpinner); - - const auto setupModel = [=]( - ModelView &to, - const Data::UniqueGiftModel &model) { - to.lifetime.destroy(); - - const auto document = model.document; - to.media = document->createMediaView(); - to.media->automaticLoad({}, nullptr); - rpl::single() | rpl::then( - document->session().downloaderTaskFinished() - ) | rpl::filter([&to] { - return to.media->loaded(); - }) | rpl::on_next([=, &to] { - const auto lottieSize = st::creditsHistoryEntryStarGiftSize; - to.lottie = ChatHelpers::LottiePlayerFromDocument( - to.media.get(), - ChatHelpers::StickerLottieSize::MessageHistory, - QSize(lottieSize, lottieSize), - Lottie::Quality::High); - - to.lifetime.destroy(); - const auto lottie = to.lottie.get(); - lottie->updates() | rpl::on_next([=] { - if (state->now.model.lottie.get() == lottie - || state->crossfade.animating()) { - cover->update(); - } - if (const auto onstack = state->checkSpinnerStart) { - onstack(); - } - }, to.lifetime); - }, to.lifetime); - }; - const auto setupPattern = [=]( - PatternView &to, - const Data::UniqueGiftPattern &pattern) { - const auto document = pattern.document; - const auto callback = [=] { - if (state->now.pattern.document == document - || state->crossfade.animating()) { - cover->update(); - } - if (const auto onstack = state->checkSpinnerStart) { - onstack(); - } - }; - to.document = document; - to.emoji = document->owner().customEmojiManager().create( - document, - callback, - Data::CustomEmojiSizeTag::Large); - [[maybe_unused]] const auto preload = to.emoji->ready(); - }; - - if (const auto spinner = state->spinner.get()) { - const auto fillBackdrops = [=] { - state->spinnerBackdrops.clear(); - state->spinnerBackdrops.reserve(kSpinnerBackdrops); - const auto push = [&](const Data::UniqueGiftBackdrop &backdrop) { - if (state->spinnerBackdrops.size() >= kSpinnerBackdrops) { - return false; - } - const auto already = ranges::contains( - state->spinnerBackdrops, - backdrop, - &BackdropView::colors); - if (!already) { - state->spinnerBackdrops.push_back({ backdrop }); - } - return true; - }; - push(spinner->target->backdrop); - for (const auto &backdrop : spinner->attributes.backdrops) { - if (!push(backdrop)) { - break; - } - } - }; - const auto fillPatterns = [=] { - state->spinnerPatterns.clear(); - state->spinnerPatterns.reserve(kSpinnerPatterns); - const auto push = [&](const Data::UniqueGiftPattern &pattern) { - if (state->spinnerPatterns.size() >= kSpinnerPatterns) { - return false; - } - const auto already = ranges::contains( - state->spinnerPatterns, - pattern.document.get(), - &PatternView::document); - if (!already) { - setupPattern( - state->spinnerPatterns.emplace_back(), - pattern); - } - return true; - }; - push(spinner->target->pattern); - for (const auto &pattern : spinner->attributes.patterns) { - if (!push(pattern)) { - break; - } - } - }; - const auto fillModels = [=] { - state->spinnerModels.clear(); - state->spinnerModels.reserve(kSpinnerModels); - const auto push = [&](const Data::UniqueGiftModel &model) { - if (state->spinnerModels.size() >= kSpinnerModels) { - return false; - } - const auto already = ranges::contains( - state->spinnerModels, - model.document, - [](const ModelView &view) { - return view.media->owner(); - }); - if (!already) { - setupModel( - state->spinnerModels.emplace_back(), - model); - } - return true; - }; - push(spinner->target->model); - for (const auto &model : spinner->attributes.models) { - if (!push(model)) { - break; - } - } - }; - state->checkSpinnerStart = [=] { - if (spinner->state.current() != SpinnerState::Loading - || state->crossfading) { - return; - } - for (const auto &pattern : state->spinnerPatterns) { - if (!pattern.emoji->ready()) { - return; - } - } - for (const auto &model : state->spinnerModels) { - if (!model.lottie || !model.lottie->ready()) { - return; - } - } - spinner->state = SpinnerState::Prepared; - state->checkSpinnerStart = nullptr; - }; - spinner->state.value() | rpl::on_next([=](SpinnerState now) { - if (now == SpinnerState::Preparing) { - fillBackdrops(); - fillPatterns(); - fillModels(); - spinner->state = SpinnerState::Loading; - if (!state->crossfading) { - state->next = {}; - } - state->checkSpinnerStart(); - } else if (now == SpinnerState::Started) { - const auto repaint = [=] { cover->update(); }; - state->backdropSpin.startWithin( - state->spinnerBackdrops.size(), - repaint); - state->patternSpin.startWithin( - state->spinnerPatterns.size(), - repaint); - state->modelSpin.startWithin( - state->spinnerModels.size(), - repaint); - state->spinStarted = crl::now(); - } - }, cover->lifetime()); - } - - const auto lottieSize = st::creditsHistoryEntryStarGiftSize; - rpl::duplicate( - data - ) | rpl::on_next([=](const UniqueGiftCover &now) { - const auto setup = [&](GiftView &to) { - Expects(!now.spinner); - - to = {}; - to.gift = now.values; - to.forced = now.force; - to.backdrop.colors = now.values.backdrop; - setupModel(to.model, now.values.model); - setupPattern(to.pattern, now.values.pattern); - }; - - const auto spinner = state->spinner.get(); - if (!state->now.gift) { - setup(state->now); - cover->update(); - } else if (now.spinner) { - Assert(spinner != nullptr); - Assert(spinner->state.current() == SpinnerState::Prepared); - - spinner->state = SpinnerState::Started; - } else if (!state->next.gift || now.force) { - const auto spinnerState = spinner - ? spinner->state.current() - : SpinnerState::Initial; - if (spinnerState == SpinnerState::Initial) { - setup(state->next); - } - } - }, cover->lifetime()); - - const auto repaintedHook = args.repaintedHook; - const auto updateLinkFg = args.subtitleLinkColored; - const auto updateColorsFromBackdrops = [=]( - const BackdropView &from, - const BackdropView &to, - float64 progress) { - released->bg = (progress == 0.) - ? from.colors.patternColor - : (progress == 1.) - ? to.colors.patternColor - : anim::color( - from.colors.patternColor, - to.colors.patternColor, - progress); - const auto color = (progress == 0.) - ? from.colors.textColor - : (progress == 1.) - ? to.colors.textColor - : anim::color( - from.colors.textColor, - to.colors.textColor, - progress); - if (updateLinkFg) { - released->link.update(color); - } - released->fg = color; - released->subtitle->setTextColorOverride(color); - }; - const auto updateColors = [=](float64 progress) { - if (repaintedHook) { - repaintedHook(state->now.gift, state->next.gift, progress); - } - updateColorsFromBackdrops( - state->now.backdrop, - state->next.backdrop, - progress); - }; - if (args.resalePrice) { - auto background = rpl::duplicate( - data - ) | rpl::map([=](const UniqueGiftCover &cover) { - auto result = cover.values.backdrop.patternColor; - result.setAlphaF(kGradientButtonBgOpacity * result.alphaF()); - return result; - }); - SetupResalePriceButton( - cover, - std::move(background), - std::move(args.resalePrice), - std::move(args.resaleClick)); - } - - const auto pretitle = args.pretitle - ? CreateChild( - cover, - std::move(args.pretitle), - st::uniqueGiftPretitle) - : nullptr; - if (pretitle) { - released->stars.emplace( - cover, - true, - Ui::Premium::MiniStarsType::SlowStars); - const auto white = QColor(255, 255, 255); - released->stars->setColorOverride(QGradientStops{ - { 0., anim::with_alpha(white, .3) }, - { 1., white }, - }); - pretitle->geometryValue() | rpl::on_next([=](QRect rect) { - const auto half = rect.height() / 2; - released->stars->setCenter(rect - QMargins(half, 0, half, 0)); - }, pretitle->lifetime()); - pretitle->setAttribute(Qt::WA_TransparentForMouseEvents); - pretitle->setTextColorOverride(QColor(255, 255, 255)); - pretitle->paintOn([=](QPainter &p) { - auto hq = PainterHighQualityEnabler(p); - const auto radius = pretitle->height() / 2.; - p.setPen(Qt::NoPen); - auto bg = released->bg; - bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF()); - p.setBrush(bg); - p.drawRoundedRect(pretitle->rect(), radius, radius); - p.translate(-pretitle->pos()); - released->stars->paint(p); - }); - } - const auto title = CreateChild( - cover, - rpl::duplicate( - data - ) | rpl::map([](const UniqueGiftCover &now) { - return now.values.title; - }), - st::uniqueGiftTitle); - title->setTextColorOverride(QColor(255, 255, 255)); - released->by = rpl::duplicate( - data - ) | rpl::map([](const UniqueGiftCover &cover) { - return cover.values.releasedBy; - }); - released->subtitleText = rpl::combine( - args.subtitle ? std::move(args.subtitle) : rpl::single(tr::marked()), - rpl::duplicate(data) - ) | rpl::map([=](TextWithEntities custom, const UniqueGiftCover &cover) { - if (!custom.empty()) { - return custom; - } - const auto &gift = cover.values; - return gift.releasedBy - ? tr::lng_gift_unique_number_by( - tr::now, - lt_index, - tr::marked(QString::number(gift.number)), - lt_name, - tr::link('@' + gift.releasedBy->username()), - tr::marked) - : tr::lng_gift_unique_number( - tr::now, - lt_index, - tr::marked(QString::number(gift.number)), - tr::marked); - }); - released->by.value() | rpl::on_next([=](PeerData *by) { - released->outlined = by || args.subtitleOutlined; - released->st = released->outlined - ? st::uniqueGiftReleasedBy - : st::uniqueGiftSubtitle; - released->st.palette.linkFg = released->link.color(); - - const auto session = &state->now.gift->model.document->session(); - released->subtitle = base::make_unique_q( - cover, - released->subtitleText.value(), - released->st, - st::defaultPopupMenu, - Core::TextContext({ .session = session })); - const auto subtitle = released->subtitle.get(); - subtitle->show(); - - cover->widthValue( - ) | rpl::on_next([=](int width) { - const auto skip = st::uniqueGiftBottom; - if (width <= 3 * skip) { - return; - } - const auto available = width - 2 * skip; - subtitle->resizeToWidth(available); - }, subtitle->lifetime()); - released->subtitleHeight = subtitle->heightValue(); - - if (!args.subtitleOutlined && args.subtitleClick) { - const auto handler = args.subtitleClick; - subtitle->setClickHandlerFilter([=](const auto &...) { - handler(); - return false; - }); - } else if (released->outlined) { - released->subtitleButton = base::make_unique_q( - cover); - const auto button = released->subtitleButton.get(); - - button->show(); - subtitle->raise(); - subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); - - button->setClickedCallback(args.subtitleClick - ? args.subtitleClick - : [=] { GiftReleasedByHandler(by); }); - subtitle->geometryValue( - ) | rpl::on_next([=](QRect geometry) { - button->setGeometry( - geometry.marginsAdded(st::giftBoxReleasedByMargin)); - }, button->lifetime()); - button->paintRequest() | rpl::on_next([=] { - auto p = QPainter(button); - auto hq = PainterHighQualityEnabler(p); - const auto use = subtitle->textMaxWidth(); - const auto add = button->width() - subtitle->width(); - const auto full = use + add; - const auto left = (button->width() - full) / 2; - const auto height = button->height(); - const auto radius = height / 2.; - p.setPen(Qt::NoPen); - p.setBrush(released->bg); - p.setOpacity(0.5); - p.drawRoundedRect(left, 0, full, height, radius, radius); - }, button->lifetime()); - } else { - released->subtitleButton = nullptr; - } - updateColors(state->crossfade.value(state->crossfading ? 1. : 0.)); - }, title->lifetime()); - - const auto attrs = args.attributesInfo - ? CreateChild(cover) - : nullptr; - auto updateAttrs = Fn([](const auto &) { - }); - if (attrs) { - struct AttributeState { - Ui::Text::String name; - Ui::Text::String type; - Ui::Text::String percent; - }; - struct AttributesState { - AttributeState model; - AttributeState pattern; - AttributeState backdrop; - }; - const auto astate = cover->lifetime().make_state(); - const auto setType = [&](AttributeState &state, tr::phrase<> text) { - state.type = Ui::Text::String( - st::uniqueAttributeType, - text(tr::now)); - }; - setType(astate->model, tr::lng_auction_preview_model); - setType(astate->pattern, tr::lng_auction_preview_symbol); - setType(astate->backdrop, tr::lng_auction_preview_backdrop); - - updateAttrs = [=](const Data::UniqueGift &gift) { - const auto set = [&]( - AttributeState &state, - const Data::UniqueGiftAttribute &value) { - state.name = Ui::Text::String( - st::uniqueAttributeName, - value.name); - state.percent = Ui::Text::String( - st::uniqueAttributePercent, - QString::number(value.rarityPermille / 10.) + '%'); - }; - set(astate->model, gift.model); - set(astate->pattern, gift.pattern); - set(astate->backdrop, gift.backdrop); - attrs->update(); - }; - const auto height = st::uniqueAttributeTop - + st::uniqueAttributePadding.top() - + st::uniqueAttributeName.font->height - + st::uniqueAttributeType.font->height - + st::uniqueAttributePadding.bottom(); - attrs->resize(attrs->width(), height); - attrs->paintOn([=](QPainter &p) { - const auto skip = st::giftBoxGiftSkip.x(); - const auto available = attrs->width() - 2 * skip; - const auto single = available / 3; - if (single <= 0) { - return; - } - auto hq = PainterHighQualityEnabler(p); - auto bg = released->bg; - bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF()); - const auto innert = st::uniqueAttributeTop; - const auto innerh = height - innert; - const auto radius = innerh / 3.; - const auto paint = [&](int x, const AttributeState &state) { - p.setPen(Qt::NoPen); - p.setBrush(bg); - p.drawRoundedRect(x, innert, single, innerh, radius, radius); - p.setPen(QColor(255, 255, 255)); - const auto padding = st::uniqueAttributePadding; - const auto inner = single - padding.left() - padding.right(); - const auto namew = std::min(inner, state.name.maxWidth()); - state.name.draw(p, { - .position = QPoint( - x + (single - namew) / 2, - innert + padding.top()), - .availableWidth = namew, - .elisionLines = 1, - }); - p.setPen(released->fg); - const auto typew = std::min(inner, state.type.maxWidth()); - state.type.draw(p, { - .position = QPoint( - x + (single - typew) / 2, - innert + padding.top() + state.name.minHeight()), - .availableWidth = typew, - }); - p.setPen(Qt::NoPen); - p.setBrush(anim::color(released->bg, released->fg, 0.3)); - const auto r = st::uniqueAttributePercent.font->height / 2.; - const auto left = x + single - state.percent.maxWidth(); - const auto top = st::uniqueAttributePercentPadding.top(); - const auto percent = QRect( - left, - top, - state.percent.maxWidth(), - st::uniqueAttributeType.font->height); - p.drawRoundedRect( - percent.marginsAdded(st::uniqueAttributePercentPadding), - r, - r); - p.setPen(QColor(255, 255, 255)); - state.percent.draw(p, { - .position = percent.topLeft(), - }); - }; - auto left = 0; - paint(left, astate->model); - paint(left + single + skip, astate->backdrop); - paint(attrs->width() - single - left, astate->pattern); - }); - } - updateAttrs(*state->now.gift); - - rpl::combine( - cover->widthValue(), - released->subtitleHeight.value() - ) | rpl::on_next([=](int width, int subtitleHeight) { - const auto skip = st::uniqueGiftBottom; - if (width <= 3 * skip) { - return; - } - const auto available = width - 2 * skip; - title->resizeToWidth(available); - - auto top = st::uniqueGiftTitleTop; - if (pretitle) { - pretitle->move((width - pretitle->width()) / 2, top); - top += pretitle->height() - + (st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop) - - title->height(); - } - - title->moveToLeft(skip, top); - if (pretitle) { - top += title->height() + st::defaultVerticalListSkip; - } else { - top += st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop; - } - - released->subtitle->moveToLeft(skip, top); - top += subtitleHeight + (skip / 2); - - if (attrs) { - attrs->resizeToWidth(width - - st::giftBoxPadding.left() - - st::giftBoxPadding.right()); - attrs->moveToLeft(st::giftBoxPadding.left(), top); - top += attrs->height() + (skip / 2); - } else { - top += (skip / 2); - } - if (!cover->height() || cover->height() == top) { - cover->resize(width, top); - } else { - state->heightFinal = top; - state->heightAnimation.start([=] { - cover->resize( - width, - int(base::SafeRound(state->heightAnimation.value(top)))); - }, cover->height(), top, st::slideWrapDuration); - } - }, cover->lifetime()); - - cover->paintOn([=](QPainter &p) { - auto progress = state->crossfade.value(state->crossfading ? 1. : 0.); - if (state->updateAttributesPending && progress >= 0.5) { - state->updateAttributesPending = false; - updateAttrs(*state->next.gift); - } else if (state->updateAttributesPending - && !state->crossfade.animating()) { - state->updateAttributesPending = false; - updateAttrs(*state->now.gift); - } - if (state->crossfading) { - updateColors(progress); - } - if (progress == 1.) { - state->crossfading = false; - state->now = base::take(state->next); - progress = 0.; - } - const auto width = cover->width(); - const auto getBackdrop = [&](BackdropView &backdrop) { - const auto ratio = style::DevicePixelRatio(); - const auto gradientSize = QSize( - width, - std::max(cover->height(), state->heightFinal)); - auto &gradient = backdrop.gradient; - if (gradient.size() != gradientSize * ratio) { - gradient = Ui::CreateTopBgGradient( - gradientSize, - backdrop.colors); - } - return gradient; - }; - const auto paintPattern = [&]( - QPainter &p, - PatternView &pattern, - const BackdropView &backdrop, - float64 shown) { - const auto color = backdrop.colors.patternColor; - const auto key = (color.red() << 16) - | (color.green() << 8) - | color.blue(); - Ui::PaintBgPoints( - p, - Ui::PatternBgPoints(), - pattern.emojis[key], - pattern.emoji.get(), - color, - QRect(0, 0, width, st::uniqueGiftSubtitleTop), - shown); - }; - const auto paintModel = [&]( - QPainter &p, - ModelView &model, - float64 scale = 1., - bool paused = false) { - const auto lottie = model.lottie.get(); - const auto factor = style::DevicePixelRatio(); - const auto request = Lottie::FrameRequest{ - .box = Size(lottieSize) * factor, - }; - const auto frame = (lottie && lottie->ready()) - ? lottie->frameInfo(request) - : Lottie::Animation::FrameInfo(); - if (frame.image.isNull()) { - return false; - } - const auto size = frame.image.size() / factor; - const auto rect = QRect( - QPoint((width - size.width()) / 2, st::uniqueGiftModelTop), - size); - if (scale < 1.) { - const auto origin = rect.center(); - p.translate(origin); - p.scale(scale, scale); - p.translate(-origin); - } - p.drawImage(rect, frame.image); - const auto count = lottie->framesCount(); - const auto finished = lottie->frameIndex() == (count - 1); - if (!paused) { - lottie->markFrameShown(); - } - return finished; - }; - const auto paint = [&](GiftView &gift, float64 shown) { - Expects(gift.gift.has_value()); - - p.drawImage(0, 0, getBackdrop(gift.backdrop)); - if (gift.gift->pattern.document != gift.gift->model.document) { - paintPattern(p, gift.pattern, gift.backdrop, shown); - } - return paintModel(p, gift.model); - }; - - if (state->spinStarted) { - const auto select = [&](auto &&list, int index, auto &fallback) - -> decltype(auto) { - return (index >= 0) ? list[index] : fallback; - }; - - const auto now = crl::now(); - const auto elapsed = now - state->spinStarted; - auto current = state->spinner->state.current(); - const auto stateTo = [&](SpinnerState to) { - if (int(current) < int(to)) { - state->spinner->state = current = to; - } - }; - const auto switchTo = [&](SpinnerState to, crl::time at) { - if (elapsed >= at) { - stateTo(to); - } - }; - const auto actualize = [&]( - AttributeSpin &spin, - auto &&list, - SpinnerState checkState, - SpinnerState finishState, - int slowdown = 1) { - if (spin.progress() < 1.) { - return; - } else if (current >= checkState) { - spin.startToTarget([=] { cover->update(); }, slowdown); - stateTo(finishState); - } else { - spin.startWithin(list.size(), [=] { cover->update(); }); - } - }; - if (anim::Disabled() && current < SpinnerState::FinishedModel) { - stateTo(SpinnerState::FinishedModel); - state->backdropSpin.startToTarget([=] { cover->update(); }); - state->patternSpin.startToTarget([=] { cover->update(); }); - state->modelSpin.startToTarget([=] { cover->update(); }); - } - if (state->backdropSpin.willIndex != 0) { - switchTo(SpinnerState::FinishBackdrop, kBackdropStopsAt); - } - actualize( - state->backdropSpin, - state->spinnerBackdrops, - SpinnerState::FinishBackdrop, - SpinnerState::FinishedBackdrop); - - if (current == SpinnerState::FinishedBackdrop - && state->patternSpin.willIndex != 0) { - switchTo(SpinnerState::FinishPattern, kPatternStopsAt); - } - actualize( - state->patternSpin, - state->spinnerPatterns, - SpinnerState::FinishPattern, - SpinnerState::FinishedPattern); - - if (current == SpinnerState::FinishedPattern - && state->modelSpin.willIndex != 0) { - // We want to start final model move not from the middle. - switchTo(SpinnerState::FinishModel, kModelStopsAt); - } - actualize( - state->modelSpin, - state->spinnerModels, - SpinnerState::FinishModel, - SpinnerState::FinishedModel, - 2); - - auto &backdropNow = select( - state->spinnerBackdrops, - state->backdropSpin.nowIndex, - state->now.backdrop); - auto &backdropWill = select( - state->spinnerBackdrops, - state->backdropSpin.willIndex, - state->now.backdrop); - const auto backdropProgress = state->backdropSpin.progress(); - updateColorsFromBackdrops( - backdropNow, - backdropWill, - backdropProgress); - - auto &patternNow = select( - state->spinnerPatterns, - state->patternSpin.nowIndex, - state->now.pattern); - auto &patternWill = select( - state->spinnerPatterns, - state->patternSpin.willIndex, - state->now.pattern); - const auto patternProgress = state->patternSpin.progress(); - const auto paintPatterns = [&]( - QPainter &p, - const BackdropView &backdrop) { - if (patternProgress >= 1.) { - paintPattern(p, patternWill, backdrop, 1.); - } else { - paintPattern(p, patternNow, backdrop, 1. - patternProgress); - if (patternProgress > 0.) { - paintPattern(p, patternWill, backdrop, patternProgress); - } - } - }; - - if (backdropProgress >= 1.) { - p.drawImage(0, 0, getBackdrop(backdropWill)); - paintPatterns(p, backdropWill); - } else { - p.drawImage(0, 0, getBackdrop(backdropNow)); - paintPatterns(p, backdropNow); - - const auto fade = width / 2; - const auto from = anim::interpolate( - -fade, - width, - backdropProgress); - if (const auto till = from + fade; till > 0) { - auto faded = getBackdrop(backdropWill); - auto q = QPainter(&faded); - paintPatterns(q, backdropWill); - - q.setCompositionMode( - QPainter::CompositionMode_DestinationIn); - auto brush = QLinearGradient(from, 0, till, 0); - brush.setStops({ - { 0., QColor(255, 255, 255, 255) }, - { 1., QColor(255, 255, 255, 0) }, - }); - const auto ratio = int(faded.devicePixelRatio()); - const auto height = faded.height() / ratio; - q.fillRect(from, 0, fade, height, brush); - q.end(); - - p.drawImage( - QRect(0, 0, till, height), - faded, - QRect(0, 0, till * ratio, faded.height())); - } - } - - auto &modelWas = select( - state->spinnerModels, - state->modelSpin.wasIndex, - state->now.model); - auto &modelNow = select( - state->spinnerModels, - state->modelSpin.nowIndex, - state->now.model); - auto &modelWill = select( - state->spinnerModels, - state->modelSpin.willIndex, - state->now.model); - const auto modelProgress = state->modelSpin.progress(); - const auto paintOne = [&](ModelView &view, float64 progress) { - if (progress >= 1. || progress <= -1.) { - return; - } - auto scale = 1.; - if (progress != 0.) { - const auto shift = progress * width / 2.; - const auto shown = 1. - std::abs(progress); - scale = kModelScaleFrom + shown * (1. - kModelScaleFrom); - p.save(); - p.setOpacity(shown); - p.translate(int(base::SafeRound(shift)), 0); - } - paintModel(p, view, scale, true); - if (progress != 0.) { - p.restore(); - } - }; - const auto initial = (state->modelSpin.nowIndex < 0); - const auto ending = (current == SpinnerState::FinishedModel) - && !state->modelSpin.willIndex; - const auto willProgress = ending - ? (modelProgress - 1.) - : (-1. + modelProgress * 2 / 3.); - const auto nowProgress = ending - ? (modelProgress * 4 / 3. - 1 / 3.) - : initial - ? (modelProgress * 1 / 3.) - : (willProgress + 2 / 3.); - const auto wasProgress = ending - ? std::min(nowProgress + 2 / 3., 1.) - : 1. + (modelProgress - 1.) * 2 / 3.; - if (!initial) { - paintOne(modelWas, wasProgress); - } - paintOne(modelNow, nowProgress); - paintOne(modelWill, willProgress); - - if (anim::Disabled() - || (ending - && modelProgress >= 1. - && backdropProgress >= 1. - && patternProgress >= 1.)) { - const auto take = [&](auto &&list) { - auto result = std::move(list.front()); - list.clear(); - return result; - }; - state->now.gift = *state->spinner->target; - state->now.backdrop = take(state->spinnerBackdrops); - state->now.pattern = take(state->spinnerPatterns); - state->now.model = take(state->spinnerModels); - state->now.forced = true; - state->spinStarted = 0; - state->spinner->state = SpinnerState::Finished; - } - } else { - if (progress < 1.) { - const auto finished = paint(state->now, 1. - progress) - || (state->next.forced - && (!state->crossfading - || !state->crossfade.animating())); - const auto next = finished - ? state->next.model.lottie.get() - : nullptr; - if (next && next->ready()) { - state->crossfading = true; - state->updateAttributesPending = true; - state->crossfade.start([=] { - cover->update(); - }, 0., 1., kCrossfadeDuration); - } else if (finished) { - if (const auto onstack = state->checkSpinnerStart) { - onstack(); - } - } - } - if (progress > 0.) { - p.setOpacity(progress); - paint(state->next, progress); - } - } - }); -} - void AddWearGiftCover( not_null container, const Data::UniqueGift &data, @@ -3933,17 +2514,20 @@ void AttachGiftSenderBadge( not_null box, std::shared_ptr show, not_null from, - const QDateTime &date) { + const QDateTime &date, + bool crafted) { const auto parent = box->getDelegate()->outerContainer(); const auto dateText = tr::bold(langDayOfMonth(date.date())); const auto badge = CreateChild( parent, (from->isSelf() - ? tr::lng_gift_unique_sender_you( - lt_date, - rpl::single(dateText), - tr::marked) + ? (crafted + ? tr::lng_gift_unique_crafter_you + : tr::lng_gift_unique_sender_you)( + lt_date, + rpl::single(dateText), + tr::marked) : tr::lng_gift_unique_sender( lt_from, rpl::single(tr::link(tr::bold(from->shortName()), 1)), @@ -4604,24 +3188,6 @@ void ShowOfferBuyBox( show->show(std::move(priceBox)); } -void GiftReleasedByHandler(not_null peer) { - const auto session = &peer->session(); - const auto window = session->tryResolveWindow(peer); - if (window) { - window->showPeerHistory(peer); - return; - } - const auto account = not_null(&session->account()); - if (const auto window = Core::App().windowFor(account)) { - window->invokeForSessionController( - &session->account(), - peer, - [=](not_null window) { - window->showPeerHistory(peer); - }); - } -} - struct UpgradeArgs : StarGiftUpgradeArgs { std::vector models; std::vector patterns; @@ -4771,7 +3337,14 @@ void AddUpgradeGiftCover( const auto null = nullptr; show->show(Box(StarGiftPreviewBox, title, list, type, null)); }; + auto numberText = state->upgraded.value( + ) | rpl::map([](const std::shared_ptr &v) { + return (v && v->info.unique && v->info.unique->number > 0) + ? u"#"_q + Lang::FormatCountDecimal(v->info.unique->number) + : QString(); + }); AddUniqueGiftCover(container, std::move(gifts), { + .numberText = std::move(numberText), .subtitle = std::move(subtitle), .subtitleClick = hasAll ? showAll : Fn(), .subtitleLinkColored = hasAll, @@ -4782,6 +3355,48 @@ void AddUpgradeGiftCover( }); } +Data::CreditsHistoryEntry EntryForUpgradedGift( + const std::shared_ptr &gift, + uint64 nextToUpgradeStickerId, + Fn nextToUpgradeShow, + Fn craftAnother) { + Expects(gift != nullptr); + + const auto unique = gift->info.unique; + const auto chatGiftPeer = gift->manageId.chat(); + const auto selfId = gift->info.document->session().userPeerId(); + return { + //.description = data.message, + .date = base::unixtime::parse(gift->date), + .credits = CreditsAmount(gift->info.stars), + .bareMsgId = uint64(gift->manageId.userMessageId().bare), + //.barePeerId = data.fromId.value, + .bareGiftStickerId = gift->info.document->id, + .bareGiftOwnerId = unique->ownerId.value, + .bareGiftHostId = unique->hostId.value, + //.bareActorId = data.fromId.value, + .bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0, + .giftChannelSavedId = gift->manageId.chatSavedId(), + .stargiftId = gift->info.id, + .giftTitle = gift->info.resellTitle, + .uniqueGift = unique, + .nextToUpgradeStickerId = nextToUpgradeStickerId, + .nextToUpgradeShow = nextToUpgradeShow, + .craftAnotherCallback = craftAnother, + .peerType = Data::CreditsHistoryEntry::PeerType::Peer, + .limitedCount = gift->info.limitedCount, + .limitedLeft = gift->info.limitedLeft, + .starsToUpgrade = int(gift->info.starsToUpgrade), + .starsForDetailsRemove = int(gift->starsForDetailsRemove), + .giftNumber = unique->number, + .converted = false, + .stargift = true, + .savedToProfile = gift->saved, + .in = (unique->ownerId == selfId), + .gift = true, + }; +} + void SwitchToUpgradedAnimation( not_null window, std::shared_ptr gift, @@ -4818,8 +3433,6 @@ void SwitchToUpgradedAnimation( const auto show = window->uiShow(); const auto unique = gift->info.unique; - const auto selfId = show->session().userPeerId(); - const auto chatGiftPeer = gift->manageId.chat(); const auto nextToUpgradeStickerId = upgradeNext ? upgradeNext->info.document->id : uint64(); @@ -4831,42 +3444,11 @@ void SwitchToUpgradedAnimation( *upgradeNext); } : Fn(); - const auto entry = Data::CreditsHistoryEntry{ - //.description = data.message, - .date = base::unixtime::parse(gift->date), - .credits = CreditsAmount(gift->info.stars), - .bareMsgId = uint64(gift->manageId.userMessageId().bare), - //.barePeerId = data.fromId.value, - .bareGiftStickerId = gift->info.document->id, - .bareGiftOwnerId = unique->ownerId.value, - .bareGiftHostId = unique->hostId.value, - //.bareActorId = data.fromId.value, - .bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0, - .giftChannelSavedId = gift->manageId.chatSavedId(), - .stargiftId = gift->info.id, - .giftTitle = gift->info.resellTitle, - .uniqueGift = unique, - .nextToUpgradeStickerId = nextToUpgradeStickerId, - .nextToUpgradeShow = nextToUpgradeShow, - .peerType = Data::CreditsHistoryEntry::PeerType::Peer, - .limitedCount = gift->info.limitedCount, - .limitedLeft = gift->info.limitedLeft, - .starsToUpgrade = int(gift->info.starsToUpgrade), - .starsForDetailsRemove = int(gift->starsForDetailsRemove), - .giftNumber = unique->number, - .converted = false, - .stargift = true, - .savedToProfile = gift->saved, - .in = (unique->ownerId == selfId), - .gift = true, - }; - - Settings::GenericCreditsEntryBody( - box, - show, - entry, - {}, - spinner); + const auto entry = EntryForUpgradedGift( + gift, + nextToUpgradeStickerId, + nextToUpgradeShow); + Settings::GenericCreditsEntryBody(box, show, entry, {}, spinner); content->resizeToWidth(st::boxWideWidth); @@ -5224,60 +3806,54 @@ void UpgradeBox( AddSkip(container, st::defaultVerticalListSkip * 2); - const auto infoRow = [&]( - rpl::producer title, - rpl::producer text, - not_null icon) { - auto raw = container->add( - object_ptr(container)); - raw->add( - object_ptr( - raw, - std::move(title) | rpl::map(tr::bold), - st::defaultFlatLabel), - st::settingsPremiumRowTitlePadding); - raw->add( - object_ptr( - raw, - std::move(text), - st::upgradeGiftSubtext), - st::settingsPremiumRowAboutPadding); - object_ptr( - raw, - *icon, - st::starrefInfoIconPosition); + const auto features = std::vector{ + { + st::menuIconUnique, + tr::lng_gift_upgrade_unique_title(tr::now), + (args.savedId + ? tr::lng_gift_upgrade_unique_about(tr::now, tr::marked) + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_unique_about_channel + : tr::lng_gift_upgrade_unique_about_user)( + tr::now, + lt_name, + tr::marked(args.peer->shortName()), + tr::marked)), + }, + { + st::menuIconReplace, + tr::lng_gift_upgrade_transferable_title(tr::now), + (args.savedId + ? tr::lng_gift_upgrade_transferable_about( + tr::now, + tr::marked) + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_transferable_about_channel + : tr::lng_gift_upgrade_transferable_about_user)( + tr::now, + lt_name, + tr::marked(args.peer->shortName()), + tr::marked)), + }, + { + st::menuIconTradable, + tr::lng_gift_upgrade_tradable_title(tr::now), + (args.savedId + ? tr::lng_gift_upgrade_tradable_about(tr::now, tr::marked) + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_tradable_about_channel + : tr::lng_gift_upgrade_tradable_about_user)( + tr::now, + lt_name, + tr::marked(args.peer->shortName()), + tr::marked)), + }, }; - - infoRow( - tr::lng_gift_upgrade_unique_title(), - (args.savedId - ? tr::lng_gift_upgrade_unique_about() - : (args.peer->isBroadcast() - ? tr::lng_gift_upgrade_unique_about_channel - : tr::lng_gift_upgrade_unique_about_user)( - lt_name, - rpl::single(args.peer->shortName()))), - &st::menuIconUnique); - infoRow( - tr::lng_gift_upgrade_transferable_title(), - (args.savedId - ? tr::lng_gift_upgrade_transferable_about() - : (args.peer->isBroadcast() - ? tr::lng_gift_upgrade_transferable_about_channel - : tr::lng_gift_upgrade_transferable_about_user)( - lt_name, - rpl::single(args.peer->shortName()))), - &st::menuIconReplace); - infoRow( - tr::lng_gift_upgrade_tradable_title(), - (args.savedId - ? tr::lng_gift_upgrade_tradable_about() - : (args.peer->isBroadcast() - ? tr::lng_gift_upgrade_tradable_about_channel - : tr::lng_gift_upgrade_tradable_about_user)( - lt_name, - rpl::single(args.peer->shortName()))), - &st::menuIconTradable); + for (const auto &feature : features) { + container->add( + Ui::MakeFeatureListEntry(container, feature), + st::boxRowPadding); + } const auto gifting = !args.savedId && !args.giftPrepayUpgradeHash.isEmpty(); @@ -5793,16 +4369,356 @@ CreditsAmount TonFromStars( CreditsType::Ton); } -object_ptr MakeGiftsSendList( +struct DefaultGiftHandlerState { + not_null window; + not_null peer; + std::shared_ptr api; + std::shared_ptr transferRequested; + uint64 resaleRequestingId = 0; + rpl::lifetime resaleLifetime; + + base::has_weak_ptr guard; +}; + +void DefaultGiftHandler( not_null window, - not_null peer, - rpl::producer gifts, - Fn loadMore) { - return MakeGiftsList( - window, - peer, - std::move(gifts), - std::move(loadMore)); + not_null state, + Info::PeerGifts::GiftDescriptor descriptor) { + const auto star = std::get_if(&descriptor); + const auto send = crl::guard(&state->guard, [=] { + window->show(Box( + SendGiftBox, + window, + state->peer, + state->api, + descriptor, + nullptr)); + }); + const auto peer = state->peer; + const auto unique = star ? star->info.unique : nullptr; + const auto premiumNeeded = star && star->info.requirePremium; + if (unique && star->resale) { + window->show(Box( + Settings::GlobalStarGiftBox, + window->uiShow(), + star->info, + Settings::StarGiftResaleInfo{ + .recipientId = peer->id, + .forceTon = star->forceTon, + }, + Settings::CreditsEntryBoxStyleOverrides())); + } else if (unique && star->mine && !peer->isSelf()) { + if (ShowTransferGiftLater(window->uiShow(), unique)) { + return; + } + const auto done = [=] { + window->session().credits().load(true); + window->showPeerHistory(peer); + }; + if (state->transferRequested == unique) { + return; + } + state->transferRequested = unique; + const auto savedId = star->transferId; + using Payments::CheckoutResult; + const auto formReady = [=]( + uint64 formId, + CreditsAmount price, + std::optional failure) { + state->transferRequested = nullptr; + if (!failure && !price.stars()) { + LOG(("API Error: Bad transfer invoice currenct.")); + } else if (!failure + || *failure == CheckoutResult::Free) { + unique->starsForTransfer = failure + ? 0 + : price.whole(); + ShowTransferToBox( + window, + peer, + unique, + savedId, + done); + } else if (*failure == CheckoutResult::Cancelled) { + done(); + } + }; + RequestOurForm( + window->uiShow(), + MTP_inputInvoiceStarGiftTransfer( + Api::InputSavedStarGiftId(savedId, unique), + peer->input()), + formReady); + } else if (star && star->resale) { + const auto id = star->info.id; + if (state->resaleRequestingId == id) { + return; + } + state->resaleRequestingId = id; + state->resaleLifetime = ShowStarGiftResale( + window, + peer, + id, + star->info.resellTitle, + [=] { state->resaleRequestingId = 0; }); + } else if (star && star->info.auction()) { + if (!IsSoldOut(star->info) + && premiumNeeded + && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium(window, star->info); + } else { + const auto id = star->info.id; + if (state->resaleRequestingId == id) { + return; + } + state->resaleRequestingId = id; + state->resaleLifetime = ShowStarGiftAuction( + window, + peer, + id, + [=] { state->resaleRequestingId = 0; }, + crl::guard(&state->guard, [=] { + state->resaleLifetime.destroy(); + })); + } + } else if (star && IsSoldOut(star->info)) { + window->show(Box(SoldOutBox, window, *star)); + } else if (premiumNeeded && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium(window, star->info); + } else if (star + && star->info.lockedUntilDate + && star->info.lockedUntilDate > base::unixtime::now()) { + const auto ready = crl::guard(window, [=] { + if (premiumNeeded && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium( + window, + v::get(descriptor).info); + } else { + send(); + } + }); + CheckMaybeGiftLocked(window, star->info.id, ready); + } else if (star + && star->info.perUserTotal + && !star->info.perUserRemains) { + window->showToast({ + .text = tr::lng_gift_sent_finished( + tr::now, + lt_count, + star->info.perUserTotal, + tr::rich), + }); + } else { + send(); + } +} + +object_ptr MakeGiftsList(GiftsListArgs &&args) { + using namespace Info::PeerGifts; + + auto result = object_ptr((QWidget*)nullptr); + const auto raw = result.data(); + + const auto mode = args.mode; + const auto peer = args.peer; + const auto window = args.window; + const auto session = &window->session(); + + Data::AmPremiumValue(session) | rpl::on_next([=] { + raw->update(); + }, raw->lifetime()); + + struct State { + Delegate delegate; + DefaultGiftHandlerState handlerState; + std::vector order; + std::vector validated; + std::vector list; + std::vector> buttons; + rpl::variable visibleRange; + bool sending = false; + int perRow = 1; + }; + const auto buttonMode = (mode == GiftsListMode::Craft) + ? GiftButtonMode::Craft + : (mode == GiftsListMode::CraftResale) + ? GiftButtonMode::CraftResale + : GiftButtonMode::Full; + const auto state = raw->lifetime().make_state(State{ + .delegate = Delegate(session, buttonMode), + .handlerState = { + .window = window, + .peer = peer, + }, + }); + const auto single = state->delegate.buttonSize(); + const auto shadow = st::defaultDropdownMenu.wrap.shadow; + const auto extend = shadow.extend; + + const auto handler = args.handler + ? args.handler + : crl::guard(raw, [=](const GiftDescriptor &descriptor) { + DefaultGiftHandler(window, &state->handlerState, descriptor); + }); + + auto &packs = session->giftBoxStickersPacks(); + packs.updated() | rpl::on_next([=] { + for (const auto &button : state->buttons) { + if (const auto raw = button.get()) { + raw->update(); + } + } + }, raw->lifetime()); + + const auto loadMore = args.loadMore; + const auto alreadySelected = args.selected; + const auto rebuild = [=] { + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + const auto range = state->visibleRange.current(); + const auto count = int(state->list.size()); + + auto &buttons = state->buttons; + if (buttons.size() < count) { + buttons.resize(count); + } + auto &validated = state->validated; + validated.resize(count); + + auto x = padding.left(); + auto y = padding.top(); + const auto perRow = state->perRow; + const auto singlew = single.width() + st::giftBoxGiftSkip.x(); + const auto singleh = single.height() + st::giftBoxGiftSkip.y(); + const auto rangeFrom = range.top - y; + const auto rowFrom = std::max(rangeFrom, 0) / singleh; + const auto rangeTill = range.bottom - y + st::giftBoxGiftSkip.y(); + const auto rowTill = (std::max(rangeTill, 0) + singleh - 1) + / singleh; + Assert(rowTill >= rowFrom); + const auto first = rowFrom * perRow; + const auto last = std::min(rowTill * perRow, count); + auto checkedFrom = 0; + auto checkedTill = int(buttons.size()); + const auto ensureButton = [&](int index) { + auto &button = buttons[index]; + if (!button) { + validated[index] = false; + for (; checkedFrom != first; ++checkedFrom) { + if (buttons[checkedFrom]) { + button = std::move(buttons[checkedFrom]); + break; + } + } + } + if (!button) { + for (; checkedTill != last; ) { + --checkedTill; + if (buttons[checkedTill]) { + button = std::move(buttons[checkedTill]); + break; + } + } + } + if (!button) { + button = std::make_unique(raw, &state->delegate); + } + const auto raw = button.get(); + if (validated[index]) { + return; + } + raw->show(); + validated[index] = true; + const auto &descriptor = state->list[state->order[index]]; + if (mode == GiftsListMode::Craft) { + const auto already = ranges::contains( + alreadySelected, + v::get(descriptor).info.unique->slug, + &Data::UniqueGift::slug); + raw->toggleSelected( + already, + GiftSelectionMode::Inset, + anim::type::instant); + raw->setAttribute(Qt::WA_TransparentForMouseEvents, already); + } + raw->setDescriptor(descriptor, buttonMode); + raw->setClickedCallback([=] { + handler(descriptor); + }); + raw->setGeometry(QRect(QPoint(x, y), single), extend); + }; + y += rowFrom * singleh; + for (auto row = rowFrom; row != rowTill; ++row) { + for (auto col = 0; col != perRow; ++col) { + const auto index = row * perRow + col; + if (index >= count) { + break; + } + const auto last = !((col + 1) % perRow); + if (last) { + x = padding.left() + available - single.width(); + } + ensureButton(index); + if (last) { + x = padding.left(); + y += singleh; + } else { + x += singlew; + } + } + } + const auto till = std::min(int(buttons.size()), rowTill * perRow); + for (auto i = count; i < till; ++i) { + if (const auto button = buttons[i].get()) { + button->hide(); + } + } + + const auto page = range.bottom - range.top; + if (loadMore && page > 0 && range.bottom + page > raw->height()) { + loadMore(); + } + }; + + state->visibleRange = raw->visibleRange(); + state->visibleRange.value( + ) | rpl::on_next(rebuild, raw->lifetime()); + + std::move( + args.gifts + ) | rpl::on_next([=](const GiftsDescriptor &gifts) { + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + state->perRow = available / single.width(); + state->list = std::move(gifts.list); + state->handlerState.api = gifts.api; + + const auto count = int(state->list.size()); + state->order = ranges::views::ints + | ranges::views::take(count) + | ranges::to_vector; + state->validated.clear(); + + if (SortForBirthday(peer)) { + ranges::stable_partition(state->order, [&](int i) { + const auto &gift = state->list[i]; + const auto stars = std::get_if(&gift); + return stars && stars->info.birthday && !stars->info.unique; + }); + } + + const auto rows = (count + state->perRow - 1) / state->perRow; + const auto height = padding.top() + + (rows * single.height()) + + ((rows - 1) * st::giftBoxGiftSkip.y()) + + padding.bottom(); + raw->resize(raw->width(), height); + rebuild(); + }, raw->lifetime()); + + return result; } void SendGiftBox( @@ -5824,6 +4740,8 @@ void SendGiftBox( : Api::DisallowedGiftTypes(); const auto disallowLimited = !peer->isSelf() && (disallowed & Api::DisallowedGiftType::Limited); + const auto disallowUnique = !peer->isSelf() + && (disallowed & Api::DisallowedGiftType::Unique); box->setStyle((limited && !auction) ? st::giftLimitedBox : st::giftBox); box->setWidth(st::boxWideWidth); box->setTitle(tr::lng_gift_send_title()); @@ -5847,7 +4765,7 @@ void SendGiftBox( state->details = GiftSendDetails{ .descriptor = descriptor, .randomId = base::RandomValue(), - .upgraded = disallowLimited && (costToUpgrade > 0), + .upgraded = disallowLimited && (costToUpgrade > 0) && !disallowUnique, }; peer->updateFull(); state->messageAllowed = peer->session().changes().peerFlagsValue( @@ -5946,7 +4864,7 @@ void SendGiftBox( session, { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); if (stars) { - if (costToUpgrade > 0 && !peer->isSelf() && !disallowLimited) { + if (costToUpgrade > 0 && !peer->isSelf() && !disallowLimited && !disallowUnique) { const auto stargiftInfo = stars->info; const auto showing = std::make_shared(); AddDivider(container); @@ -6157,4 +5075,57 @@ void SendGiftBox( } } +std::shared_ptr FindUniqueGift( + not_null session, + const MTPUpdates &updates) { + auto result = std::shared_ptr(); + const auto checkAction = [&](const MTPDmessageService &message) { + const auto &action = message.vaction(); + action.match([&](const MTPDmessageActionStarGiftUnique &data) { + if (const auto gift = Api::FromTL(session, data.vgift())) { + const auto to = data.vpeer() + ? peerFromMTP(*data.vpeer()) + : PeerId(); + const auto service = data.vfrom_id() + && session->data().peer( + peerFromMTP(*data.vfrom_id()))->isServiceUser(); + const auto channel = (service && peerIsChannel(to)) + ? session->data().channel(peerToChannel(to)).get() + : nullptr; + const auto channelSavedId = channel + ? data.vsaved_id().value_or_empty() + : uint64(); + const auto realGiftMsgId = (peerIsUser(to) && data.vsaved_id()) + ? MsgId(data.vsaved_id().value_or_empty()) + : MsgId(message.vid().v); + + result = std::make_shared( + Data::GiftUpgradeResult{ + .info = *gift, + .manageId = (channel && channelSavedId) + ? Data::SavedStarGiftId::Chat( + channel, + channelSavedId) + : Data::SavedStarGiftId::User(realGiftMsgId), + .date = message.vdate().v, + .starsForDetailsRemove = int( + data.vdrop_original_details_stars( + ).value_or_empty()), + .saved = data.is_saved(), + }); + } + }, [](const auto &) {}); + }; + updates.match([&](const MTPDupdates &data) { + for (const auto &update : data.vupdates().v) { + update.match([&](const MTPDupdateNewMessage &data) { + data.vmessage().match([&](const MTPDmessageService &data) { + checkAction(data); + }, [](const auto &) {}); + }, [](const auto &) {}); + } + }, [](const auto &) {}); + return result; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index 81e77a9c1c..2ca942135b 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "boxes/star_gift_cover_box.h" #include "data/data_star_gift.h" namespace Api { @@ -65,31 +66,6 @@ void ShowStarGiftBox( not_null controller, not_null peer); -struct UniqueGiftCoverArgs { - rpl::producer pretitle; - rpl::producer subtitle; - Fn subtitleClick; - bool subtitleLinkColored = false; - bool subtitleOutlined = false; - rpl::producer resalePrice; - Fn resaleClick; - bool attributesInfo = false; - Fn now, - std::optional next, - float64 progress)> repaintedHook; - std::shared_ptr upgradeSpinner; -}; -struct UniqueGiftCover { - Data::UniqueGift values; - bool spinner = false; - bool force = false; -}; - -void AddUniqueGiftCover( - not_null container, - rpl::producer data, - UniqueGiftCoverArgs &&args); void AddWearGiftCover( not_null container, const Data::UniqueGift &data, @@ -99,7 +75,8 @@ void AttachGiftSenderBadge( not_null box, std::shared_ptr show, not_null from, - const QDateTime &date); + const QDateTime &date, + bool crafted); void ShowUniqueGiftWearBox( std::shared_ptr show, @@ -124,8 +101,6 @@ void ShowOfferBuyBox( std::shared_ptr show, std::shared_ptr unique); -void GiftReleasedByHandler(not_null peer); - struct StarGiftUpgradeArgs { not_null controller; Data::StarGift stargift; @@ -182,11 +157,21 @@ struct GiftsDescriptor { std::vector list; std::shared_ptr api; }; -[[nodiscard]] object_ptr MakeGiftsSendList( - not_null window, - not_null peer, - rpl::producer gifts, - Fn loadMore); +enum class GiftsListMode { + Send, + Craft, + CraftResale, +}; +struct GiftsListArgs { + not_null window; + GiftsListMode mode = GiftsListMode::Send; + not_null peer; + rpl::producer gifts; + std::vector> selected; + Fn loadMore; + Fn handler; +}; +[[nodiscard]] object_ptr MakeGiftsList(GiftsListArgs &&args); void SendGiftBox( not_null box, @@ -196,4 +181,14 @@ void SendGiftBox( const Info::PeerGifts::GiftDescriptor &descriptor, rpl::producer auctionState); +[[nodiscard]] Data::CreditsHistoryEntry EntryForUpgradedGift( + const std::shared_ptr &gift, + uint64 nextToUpgradeStickerId = 0, + Fn nextToUpgradeShow = nullptr, + Fn craftAnother = nullptr); + +[[nodiscard]] std::shared_ptr FindUniqueGift( + not_null session, + const MTPUpdates &updates); + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_cover_box.cpp b/Telegram/SourceFiles/boxes/star_gift_cover_box.cpp new file mode 100644 index 0000000000..d774f5f05b --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_cover_box.cpp @@ -0,0 +1,1346 @@ +/* +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/star_gift_cover_box.h" + +#include "chat_helpers/stickers_lottie.h" +#include "core/application.h" +#include "core/ui_integration.h" +#include "main/main_session.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "data/data_document.h" +#include "data/data_file_origin.h" +#include "data/data_document_media.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_common.h" +#include "lottie/lottie_single_player.h" +#include "ui/effects/premium_stars_colored.h" +#include "ui/painter.h" +#include "ui/text/format_values.h" +#include "ui/top_background_gradient.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/widgets/labels.h" +#include "info/peer_gifts/info_peer_gifts_common.h" +#include "styles/style_chat.h" +#include "styles/style_credits.h" +#include "styles/style_giveaway.h" +#include "styles/style_layers.h" + +namespace Ui { +namespace { + +constexpr auto kCrossfadeDuration = crl::time(400); +constexpr auto kGradientButtonBgOpacity = 0.6; + +constexpr auto kSpinnerBackdrops = 6; +constexpr auto kSpinnerPatterns = 6; +constexpr auto kSpinnerModels = 6; + +constexpr auto kBackdropSpinDuration = crl::time(300); +constexpr auto kBackdropStopsAt = crl::time(2.5 * 1000); +constexpr auto kPatternSpinDuration = crl::time(600); +constexpr auto kPatternStopsAt = crl::time(4 * 1000); +constexpr auto kModelSpinDuration = crl::time(160); +constexpr auto kModelStopsAt = crl::time(5.5 * 1000); +constexpr auto kModelScaleFrom = 0.7; + +struct AttributeSpin { + AttributeSpin(crl::time duration) : duration(duration) { + } + + Animations::Simple animation; + crl::time duration = 0; + int wasIndex = -1; + int nowIndex = -1; + int willIndex = -1; + + [[nodiscard]] float64 progress() const { + return animation.value(1.); + } + void startWithin(int count, Fn update) { + Expects(count > 0); + + wasIndex = nowIndex; + nowIndex = willIndex; + willIndex = (willIndex < 0 ? 1 : (willIndex + 1)) % count; + animation.start(update, 0., 1., duration); + } + void startToTarget(Fn update, int slowdown = 1) { + if (willIndex != 0) { + wasIndex = nowIndex; + nowIndex = willIndex; + willIndex = 0; + animation.start( + update, + 0., + 1., + duration * 3 * slowdown, + anim::easeOutCubic); + } + } +}; + +struct Released { + Released() : link(QColor(255, 255, 255)) { + } + + rpl::variable subtitleText; + std::optional stars; + style::owned_color link; + style::FlatLabel st; + rpl::variable by; + base::unique_qptr subtitle; + base::unique_qptr subtitleButton; + rpl::variable subtitleHeight; + rpl::variable subtitleCustom; + bool outlined = false; + QColor bg; + QColor fg; +}; + +} // namespace + +struct UniqueGiftCoverWidget::BackdropView { + Data::UniqueGiftBackdrop colors; + QImage gradient; +}; + +struct UniqueGiftCoverWidget::PatternView { + DocumentData *document = nullptr; + std::unique_ptr emoji; + base::flat_map> emojis; +}; + +struct UniqueGiftCoverWidget::ModelView { + std::shared_ptr media; + std::unique_ptr lottie; + rpl::lifetime lifetime; +}; + +struct UniqueGiftCoverWidget::GiftView { + std::optional gift; + BackdropView backdrop; + PatternView pattern; + ModelView model; + bool forced = false; +}; + +struct UniqueGiftCoverWidget::State { + std::shared_ptr spinner; + Fn checkSpinnerStart; + GiftView now; + GiftView next; + Animations::Simple crossfade; + Animations::Simple heightAnimation; + std::vector spinnerBackdrops; + std::vector spinnerPatterns; + std::vector spinnerModels; + AttributeSpin backdropSpin = kBackdropSpinDuration; + AttributeSpin patternSpin = kPatternSpinDuration; + AttributeSpin modelSpin = kModelSpinDuration; + crl::time spinStarted = 0; + int heightFinal = 0; + bool crossfading = false; + bool updateAttributesPending = false; + + Released released; + + FlatLabel *pretitle = nullptr; + FlatLabel *title = nullptr; + RpWidget *attrs = nullptr; + + Fn updateAttrs; + Fn updateColors; + Fn + updateColorsFromBackdrops; + Fn setupModel; + Fn setupPattern; + + FlatLabel *number = nullptr; + rpl::variable numberTextWidth; + QImage craftedBadge; + Info::PeerGifts::GiftBadge craftedBadgeKey; + rpl::variable resaleAmount; + Fn resaleClick; + +}; + +UniqueGiftCoverWidget::UniqueGiftCoverWidget( + QWidget *parent, + rpl::producer data, + UniqueGiftCoverArgs &&args) +: RpWidget(parent) +, _state(std::make_unique()) { + using SpinnerState = Data::GiftUpgradeSpinner::State; + + _state->spinner = std::move(args.upgradeSpinner); + + _state->setupModel = [this]( + ModelView &to, + const Data::UniqueGiftModel &model) { + to.lifetime.destroy(); + + const auto document = model.document; + to.media = document->createMediaView(); + to.media->automaticLoad(document->stickerSetOrigin(), nullptr); + rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([&to] { + return to.media->loaded(); + }) | rpl::on_next([this, &to] { + const auto lottieSize = st::creditsHistoryEntryStarGiftSize; + to.lottie = ChatHelpers::LottiePlayerFromDocument( + to.media.get(), + ChatHelpers::StickerLottieSize::MessageHistory, + QSize(lottieSize, lottieSize), + Lottie::Quality::High); + + to.lifetime.destroy(); + const auto lottie = to.lottie.get(); + lottie->updates() | rpl::on_next([this, lottie] { + if (_state->now.model.lottie.get() == lottie + || _state->crossfade.animating()) { + update(); + } + if (const auto onstack = _state->checkSpinnerStart) { + onstack(); + } + }, to.lifetime); + }, to.lifetime); + }; + + _state->setupPattern = [this]( + PatternView &to, + const Data::UniqueGiftPattern &pattern) { + const auto document = pattern.document; + const auto callback = [this, document] { + if (_state->now.pattern.document == document + || _state->crossfade.animating()) { + update(); + } + if (const auto onstack = _state->checkSpinnerStart) { + onstack(); + } + }; + to.document = document; + to.emoji = document->owner().customEmojiManager().create( + document, + callback, + Data::CustomEmojiSizeTag::Large); + [[maybe_unused]] const auto preload = to.emoji->ready(); + }; + + if (const auto spinner = _state->spinner.get()) { + const auto fillBackdrops = [this, spinner] { + _state->spinnerBackdrops.clear(); + _state->spinnerBackdrops.reserve(kSpinnerBackdrops); + const auto push = [this](const Data::UniqueGiftBackdrop &backdrop) { + if (_state->spinnerBackdrops.size() >= kSpinnerBackdrops) { + return false; + } + const auto already = ranges::contains( + _state->spinnerBackdrops, + backdrop, + &BackdropView::colors); + if (!already) { + _state->spinnerBackdrops.push_back({ backdrop }); + } + return true; + }; + push(spinner->target->backdrop); + for (const auto &backdrop : spinner->attributes.backdrops) { + if (!push(backdrop)) { + break; + } + } + }; + const auto fillPatterns = [this, spinner] { + _state->spinnerPatterns.clear(); + _state->spinnerPatterns.reserve(kSpinnerPatterns); + const auto push = [this](const Data::UniqueGiftPattern &pattern) { + if (_state->spinnerPatterns.size() >= kSpinnerPatterns) { + return false; + } + const auto already = ranges::contains( + _state->spinnerPatterns, + pattern.document.get(), + &PatternView::document); + if (!already) { + _state->setupPattern( + _state->spinnerPatterns.emplace_back(), + pattern); + } + return true; + }; + push(spinner->target->pattern); + for (const auto &pattern : spinner->attributes.patterns) { + if (!push(pattern)) { + break; + } + } + }; + const auto fillModels = [this, spinner] { + _state->spinnerModels.clear(); + _state->spinnerModels.reserve(kSpinnerModels); + const auto push = [this](const Data::UniqueGiftModel &model) { + if (_state->spinnerModels.size() >= kSpinnerModels) { + return false; + } + const auto already = ranges::contains( + _state->spinnerModels, + model.document, + [](const ModelView &view) { + return view.media->owner(); + }); + if (!already) { + _state->setupModel( + _state->spinnerModels.emplace_back(), + model); + } + return true; + }; + push(spinner->target->model); + for (const auto &model : spinner->attributes.models) { + if (!push(model)) { + break; + } + } + }; + _state->checkSpinnerStart = [this, spinner] { + if (spinner->state.current() != SpinnerState::Loading + || _state->crossfading) { + return; + } + for (const auto &pattern : _state->spinnerPatterns) { + if (!pattern.emoji->ready()) { + return; + } + } + for (const auto &model : _state->spinnerModels) { + if (!model.lottie || !model.lottie->ready()) { + return; + } + } + spinner->state = SpinnerState::Prepared; + _state->checkSpinnerStart = nullptr; + }; + spinner->state.value() | rpl::on_next([=](SpinnerState now) { + if (now == SpinnerState::Preparing) { + fillBackdrops(); + fillPatterns(); + fillModels(); + spinner->state = SpinnerState::Loading; + if (!_state->crossfading) { + _state->next = {}; + } + _state->checkSpinnerStart(); + } else if (now == SpinnerState::Started) { + const auto repaint = [this] { update(); }; + _state->backdropSpin.startWithin( + _state->spinnerBackdrops.size(), + repaint); + _state->patternSpin.startWithin( + _state->spinnerPatterns.size(), + repaint); + _state->modelSpin.startWithin( + _state->spinnerModels.size(), + repaint); + _state->spinStarted = crl::now(); + } + }, lifetime()); + } + + rpl::duplicate( + data + ) | rpl::on_next([this](const UniqueGiftCover &now) { + const auto setup = [&](GiftView &to) { + Expects(!now.spinner); + + to = {}; + to.gift = now.values; + to.forced = now.force; + to.backdrop.colors = now.values.backdrop; + _state->setupModel(to.model, now.values.model); + _state->setupPattern(to.pattern, now.values.pattern); + }; + + const auto spinner = _state->spinner.get(); + if (!_state->now.gift) { + setup(_state->now); + update(); + } else if (now.spinner) { + Assert(spinner != nullptr); + Assert(spinner->state.current() == SpinnerState::Prepared); + + spinner->state = SpinnerState::Started; + } else if (!_state->next.gift || now.force) { + const auto spinnerState = spinner + ? spinner->state.current() + : SpinnerState::Initial; + if (spinnerState == SpinnerState::Initial) { + setup(_state->next); + } + } + }, lifetime()); + + auto subtitleCustomText = args.subtitle + ? std::move(args.subtitle) + : rpl::single(tr::marked()); + _state->released.subtitleCustom = rpl::duplicate( + subtitleCustomText + ) | rpl::map([](const TextWithEntities &custom) { + return !custom.empty(); + }); + + const auto repaintedHook = args.repaintedHook; + const auto updateLinkFg = args.subtitleLinkColored; + _state->updateColorsFromBackdrops = [this, updateLinkFg]( + const BackdropView &from, + const BackdropView &to, + float64 progress) { + _state->released.bg = (progress == 0.) + ? from.colors.patternColor + : (progress == 1.) + ? to.colors.patternColor + : anim::color( + from.colors.patternColor, + to.colors.patternColor, + progress); + const auto color = (progress == 0.) + ? from.colors.textColor + : (progress == 1.) + ? to.colors.textColor + : anim::color( + from.colors.textColor, + to.colors.textColor, + progress); + if (updateLinkFg) { + const auto &subtitleCustom = _state->released.subtitleCustom; + _state->released.link.update(subtitleCustom.current() + ? color + : QColor(255, 255, 255)); + } + _state->released.fg = color; + const auto onSale = !_state->released.subtitleCustom.current() + && _state->resaleAmount.current() + && _state->resaleAmount.current().value() > 0; + _state->released.subtitle->setTextColorOverride( + onSale ? QColor(255, 255, 255) : color); + if (_state->number) { + _state->number->setTextColorOverride(color); + } + }; + _state->updateColors = [this, repaintedHook](float64 progress) { + if (repaintedHook) { + repaintedHook(_state->now.gift, _state->next.gift, progress); + } + _state->updateColorsFromBackdrops( + _state->now.backdrop, + _state->next.backdrop, + progress); + }; + + if (args.resalePrice) { + std::move(args.resalePrice) | rpl::on_next([this](CreditsAmount value) { + _state->resaleAmount = value; + }, lifetime()); + } + _state->resaleClick = std::move(args.resaleClick); + + _state->pretitle = args.pretitle + ? CreateChild( + this, + std::move(args.pretitle), + st::uniqueGiftPretitle) + : nullptr; + if (_state->pretitle) { + _state->released.stars.emplace( + this, + true, + Premium::MiniStarsType::SlowStars); + const auto white = QColor(255, 255, 255); + _state->released.stars->setColorOverride(QGradientStops{ + { 0., anim::with_alpha(white, .3) }, + { 1., white }, + }); + _state->pretitle->geometryValue() | rpl::on_next([this](QRect rect) { + const auto half = rect.height() / 2; + _state->released.stars->setCenter(rect - QMargins(half, 0, half, 0)); + }, _state->pretitle->lifetime()); + _state->pretitle->setAttribute(Qt::WA_TransparentForMouseEvents); + _state->pretitle->setTextColorOverride(QColor(255, 255, 255)); + _state->pretitle->paintOn([this](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + const auto radius = _state->pretitle->height() / 2.; + p.setPen(Qt::NoPen); + auto bg = _state->released.bg; + bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF()); + p.setBrush(bg); + p.drawRoundedRect(_state->pretitle->rect(), radius, radius); + p.translate(-_state->pretitle->pos()); + _state->released.stars->paint(p); + }); + } + + _state->title = CreateChild( + this, + rpl::duplicate( + data + ) | rpl::map([](const UniqueGiftCover &now) { + return now.values.title; + }), + st::uniqueGiftTitle); + _state->title->setTextColorOverride(QColor(255, 255, 255)); + + auto numberTextDup = args.numberText + ? rpl::duplicate(args.numberText) + : rpl::producer(); + _state->number = args.numberText + ? CreateChild( + this, + std::move(args.numberText), + st::uniqueGiftNumber) + : nullptr; + if (_state->number) { + _state->number->setTextColorOverride(QColor(255, 255, 255)); + std::move( + numberTextDup + ) | rpl::on_next([this](const QString &text) { + _state->numberTextWidth = text.isEmpty() ? 0 : 1; + }, lifetime()); + } + + _state->released.by = rpl::duplicate( + data + ) | rpl::map([](const UniqueGiftCover &cover) { + return cover.values.releasedBy; + }); + _state->released.subtitleText = rpl::combine( + std::move(subtitleCustomText), + _state->resaleAmount.value(), + rpl::duplicate(data) + ) | rpl::map([]( + TextWithEntities custom, + CreditsAmount resalePrice, + const UniqueGiftCover &cover) { + if (!custom.empty()) { + return custom; + } + const auto &gift = cover.values; + if (resalePrice && resalePrice.value() > 0) { + auto priceText = resalePrice.ton() + ? Text::IconEmoji(&st::tonIconEmojiInSmall).append( + Lang::FormatCreditsAmountDecimal(resalePrice)) + : Text::IconEmoji(&st::starIconEmojiSmall).append( + Lang::FormatCountDecimal(resalePrice.whole())); + return tr::lng_gift_on_sale_for( + tr::now, + lt_price, + priceText, + tr::marked); + } + if (gift.releasedBy) { + return tr::lng_gift_released_by( + tr::now, + lt_name, + tr::link('@' + gift.releasedBy->username()), + tr::marked); + } + return tr::marked(gift.model.name); + }); + + const auto subtitleOutlined = args.subtitleOutlined; + const auto subtitleClick = args.subtitleClick; + rpl::combine( + _state->released.by.value(), + _state->released.subtitleCustom.value(), + _state->resaleAmount.value() + ) | rpl::on_next([=](PeerData *by, bool subtitleCustom, CreditsAmount resale) { + const auto hasResale = resale && resale.value() > 0; + _state->released.outlined = (subtitleCustom && subtitleOutlined) + || (!subtitleCustom && (hasResale || by)); + _state->released.st = !_state->released.outlined + ? st::uniqueGiftSubtitle + : (!subtitleCustom && hasResale) + ? st::uniqueGiftOnSale + : st::uniqueGiftReleasedBy; + _state->released.st.palette.linkFg = _state->released.link.color(); + + const auto session = &_state->now.gift->model.document->session(); + _state->released.subtitle = base::make_unique_q( + this, + _state->released.subtitleText.value(), + _state->released.st, + st::defaultPopupMenu, + Core::TextContext({ .session = session })); + const auto subtitle = _state->released.subtitle.get(); + subtitle->show(); + + widthValue( + ) | rpl::on_next([=](int width) { + const auto skip = st::uniqueGiftBottom; + if (width <= 3 * skip) { + return; + } + const auto available = width - 2 * skip; + subtitle->resizeToWidth(available); + }, subtitle->lifetime()); + _state->released.subtitleHeight.force_assign(subtitle->height()); + _state->released.subtitleHeight = subtitle->heightValue(); + + const auto handler = subtitleCustom + ? (subtitleClick ? subtitleClick : Fn()) + : hasResale + ? _state->resaleClick + : by + ? Fn([=] { GiftReleasedByHandler(by); }) + : Fn(); + + if (!_state->released.outlined && handler) { + subtitle->setClickHandlerFilter([=](const auto &...) { + handler(); + return false; + }); + } else if (_state->released.outlined) { + _state->released.subtitleButton = base::make_unique_q( + this); + const auto button = _state->released.subtitleButton.get(); + + button->show(); + subtitle->raise(); + subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + + if (handler) { + button->setClickedCallback(handler); + } else { + button->setAttribute(Qt::WA_TransparentForMouseEvents); + } + subtitle->geometryValue( + ) | rpl::on_next([=](QRect geometry) { + button->setGeometry( + geometry.marginsAdded(st::giftBoxReleasedByMargin)); + }, button->lifetime()); + button->paintRequest() | rpl::on_next([=] { + auto p = QPainter(button); + auto hq = PainterHighQualityEnabler(p); + const auto use = subtitle->textMaxWidth(); + const auto add = button->width() - subtitle->width(); + const auto full = use + add; + const auto left = (button->width() - full) / 2; + const auto height = button->height(); + const auto radius = height / 2.; + p.setPen(Qt::NoPen); + p.setBrush(_state->released.bg); + p.setOpacity(0.5); + p.drawRoundedRect(left, 0, full, height, radius, radius); + }, button->lifetime()); + } else { + _state->released.subtitleButton = nullptr; + } + _state->updateColors(_state->crossfade.value( + _state->crossfading ? 1. : 0.)); + }, _state->title->lifetime()); + + _state->attrs = args.attributesInfo + ? CreateChild(this) + : nullptr; + _state->updateAttrs = [](const Data::UniqueGift &) {}; + if (_state->attrs) { + struct AttributeState { + Text::String name; + Text::String type; + Text::String percent; + }; + struct AttributesState { + AttributeState model; + AttributeState pattern; + AttributeState backdrop; + }; + const auto astate = lifetime().make_state(); + const auto setType = [&](AttributeState &state, tr::phrase<> text) { + state.type = Text::String( + st::uniqueAttributeType, + text(tr::now)); + }; + setType(astate->model, tr::lng_auction_preview_model); + setType(astate->pattern, tr::lng_auction_preview_symbol); + setType(astate->backdrop, tr::lng_auction_preview_backdrop); + + _state->updateAttrs = [this, astate](const Data::UniqueGift &gift) { + const auto set = [&]( + AttributeState &state, + const Data::UniqueGiftAttribute &value) { + state.name = Text::String( + st::uniqueAttributeName, + value.name); + state.percent = Text::String( + st::uniqueAttributePercent, + Data::UniqueGiftAttributeText(value)); + }; + set(astate->model, gift.model); + set(astate->pattern, gift.pattern); + set(astate->backdrop, gift.backdrop); + _state->attrs->update(); + }; + const auto attrsHeight = st::uniqueAttributeTop + + st::uniqueAttributePadding.top() + + st::uniqueAttributeName.font->height + + st::uniqueAttributeType.font->height + + st::uniqueAttributePadding.bottom(); + _state->attrs->resize(_state->attrs->width(), attrsHeight); + _state->attrs->paintOn([this, astate, attrsHeight](QPainter &p) { + const auto skip = st::giftBoxGiftSkip.x(); + const auto available = _state->attrs->width() - 2 * skip; + const auto single = available / 3; + if (single <= 0) { + return; + } + auto hq = PainterHighQualityEnabler(p); + auto bg = _state->released.bg; + bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF()); + const auto innert = st::uniqueAttributeTop; + const auto innerh = attrsHeight - innert; + const auto radius = innerh / 3.; + const auto paint = [&](int x, const AttributeState &state) { + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawRoundedRect(x, innert, single, innerh, radius, radius); + p.setPen(QColor(255, 255, 255)); + const auto padding = st::uniqueAttributePadding; + const auto inner = single - padding.left() - padding.right(); + const auto namew = std::min(inner, state.name.maxWidth()); + state.name.draw(p, { + .position = QPoint( + x + (single - namew) / 2, + innert + padding.top()), + .availableWidth = namew, + .elisionLines = 1, + }); + p.setPen(_state->released.fg); + const auto typew = std::min(inner, state.type.maxWidth()); + state.type.draw(p, { + .position = QPoint( + x + (single - typew) / 2, + innert + padding.top() + state.name.minHeight()), + .availableWidth = typew, + }); + p.setPen(Qt::NoPen); + p.setBrush(anim::color(_state->released.bg, _state->released.fg, 0.3)); + const auto r = st::uniqueAttributePercent.font->height / 2.; + const auto left = x + single - state.percent.maxWidth(); + const auto top = st::uniqueAttributePercentPadding.top(); + const auto percent = QRect( + left, + top, + state.percent.maxWidth(), + st::uniqueAttributeType.font->height); + p.drawRoundedRect( + percent.marginsAdded(st::uniqueAttributePercentPadding), + r, + r); + p.setPen(QColor(255, 255, 255)); + state.percent.draw(p, { + .position = percent.topLeft(), + }); + }; + auto left = 0; + paint(left, astate->model); + paint(left + single + skip, astate->backdrop); + paint(_state->attrs->width() - single - left, astate->pattern); + }); + } + _state->updateAttrs(*_state->now.gift); + + rpl::combine( + widthValue(), + _state->released.subtitleHeight.value(), + _state->numberTextWidth.value() + ) | rpl::on_next([this](int width, int subtitleHeight, int) { + const auto skip = st::uniqueGiftBottom; + if (width <= 3 * skip) { + return; + } + const auto available = width - 2 * skip; + auto top = st::uniqueGiftTitleTop; + if (_state->pretitle) { + _state->title->resizeToWidth(available); + _state->pretitle->move((width - _state->pretitle->width()) / 2, top); + top += _state->pretitle->height() + + (st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop) + - _state->title->height(); + } + + if (_state->number && _state->number->textMaxWidth() > 0) { + const auto titleWidth = _state->title->textMaxWidth(); + _state->title->resizeToWidth(titleWidth); + const auto numberWidth = _state->number->textMaxWidth(); + _state->number->resizeToWidth(numberWidth); + const auto gap = st::normalFont->spacew; + const auto totalWidth = titleWidth + gap + numberWidth; + const auto groupLeft = (width - totalWidth) / 2; + _state->title->moveToLeft(groupLeft, top); + const auto &stTitle = _state->title->st(); + const auto &stNumber = _state->number->st(); + _state->number->moveToLeft( + groupLeft + titleWidth + gap, + (top + + stTitle.style.font->ascent + - stNumber.style.font->ascent)); + } else { + _state->title->resizeToWidth(available); + _state->title->moveToLeft(skip, top); + } + if (_state->pretitle) { + top += _state->title->height() + st::defaultVerticalListSkip; + } else { + top += st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop; + } + + _state->released.subtitle->moveToLeft(skip, top); + top += subtitleHeight + (skip / 2); + + if (_state->attrs) { + _state->attrs->resizeToWidth(width + - st::giftBoxPadding.left() + - st::giftBoxPadding.right()); + _state->attrs->moveToLeft(st::giftBoxPadding.left(), top); + top += _state->attrs->height() + (skip / 2); + } else { + top += (skip / 2); + } + if (!height() || height() == top) { + resize(width, top); + } else { + _state->heightFinal = top; + _state->heightAnimation.start([this, width, top] { + resize( + width, + int(base::SafeRound(_state->heightAnimation.value(top)))); + }, height(), top, st::slideWrapDuration); + } + }, lifetime()); +} + +void UniqueGiftCoverWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto progress = _state->crossfade.value(_state->crossfading ? 1. : 0.); + if (_state->updateAttributesPending && progress >= 0.5) { + _state->updateAttributesPending = false; + _state->updateAttrs(*_state->next.gift); + } else if (_state->updateAttributesPending + && !_state->crossfade.animating()) { + _state->updateAttributesPending = false; + _state->updateAttrs(*_state->now.gift); + } + if (_state->crossfading) { + _state->updateColors(progress); + } + if (progress == 1.) { + _state->crossfading = false; + _state->now = base::take(_state->next); + progress = 0.; + } + + const auto context = PaintContext{ + .width = width(), + .patternAreaHeight = st::uniqueGiftSubtitleTop, + }; + if (_state->spinStarted) { + paintSpinnerAnimation(p, context); + } else { + paintNormalAnimation(p, context, progress); + } +} + +UniqueGiftCoverWidget::~UniqueGiftCoverWidget() = default; + +QImage UniqueGiftCoverWidget::prepareBackdrop( + BackdropView &backdrop, + const PaintContext &context) { + const auto ratio = style::DevicePixelRatio(); + const auto gradientSize = QSize( + context.width, + std::max(height(), _state->heightFinal)); + if (backdrop.gradient.size() != gradientSize * ratio) { + backdrop.gradient = CreateTopBgGradient( + gradientSize, + backdrop.colors); + } + return backdrop.gradient; +} + +void UniqueGiftCoverWidget::paintBackdrop( + QPainter &p, + BackdropView &backdrop, + const PaintContext &context) { + p.drawImage(0, 0, prepareBackdrop(backdrop, context)); +} + +void UniqueGiftCoverWidget::paintPattern( + QPainter &p, + PatternView &pattern, + const BackdropView &backdrop, + const PaintContext &context, + float64 shown) { + const auto color = backdrop.colors.patternColor; + const auto key = (color.red() << 16) + | (color.green() << 8) + | color.blue(); + PaintBgPoints( + p, + PatternBgPoints(), + pattern.emojis[key], + pattern.emoji.get(), + color, + QRect(0, 0, context.width, context.patternAreaHeight), + shown); +} + +bool UniqueGiftCoverWidget::paintModel( + QPainter &p, + ModelView &model, + const PaintContext &context, + float64 scale, + bool paused) { + const auto lottieSize = st::creditsHistoryEntryStarGiftSize; + const auto lottie = model.lottie.get(); + const auto factor = style::DevicePixelRatio(); + const auto request = Lottie::FrameRequest{ + .box = QSize(lottieSize, lottieSize) * factor, + }; + const auto frame = (lottie && lottie->ready()) + ? lottie->frameInfo(request) + : Lottie::Animation::FrameInfo(); + if (frame.image.isNull()) { + return false; + } + const auto size = frame.image.size() / factor; + const auto rect = QRect( + QPoint((context.width - size.width()) / 2, st::uniqueGiftModelTop), + size); + if (scale < 1.) { + const auto origin = rect.center(); + p.translate(origin); + p.scale(scale, scale); + p.translate(-origin); + } + p.drawImage(rect, frame.image); + const auto count = lottie->framesCount(); + const auto finished = lottie->frameIndex() == (count - 1); + if (!paused) { + lottie->markFrameShown(); + } + return finished; +} + +QRect UniqueGiftCoverWidget::prepareCraftFrame( + QImage &canvas, + const CraftContext &context) { + const auto full = this->size(); + const auto ratio = style::DevicePixelRatio(); + + if (canvas.size() != full * ratio) { + canvas = QImage(full * ratio, QImage::Format_ARGB32_Premultiplied); + canvas.setDevicePixelRatio(ratio); + } + + LOG(("FULL: %1x%2").arg(full.width()).arg(full.height())); + + const auto expand = context.expandProgress; + const auto size = QSize( + anim::interpolate(context.initial.width(), full.width(), expand), + anim::interpolate(context.initial.height(), full.height(), expand)); + const auto rect = QRect(QPoint(), size); + auto p = QPainter(&canvas); + auto hq = PainterHighQualityEnabler(p); + const auto radius = st::boxRadius * expand; + const auto bgProgress = context.fadeInProgress; + + auto gradient = QRadialGradient( + rect.center(), + size.height() / 2); + const auto center = anim::color( + context.initialBg, + _state->now.backdrop.colors.centerColor, + bgProgress); + const auto edge = anim::color( + context.initialBg, + _state->now.backdrop.colors.edgeColor, + bgProgress); + gradient.setStops({ { 0., center }, { 1., edge } }); + p.setPen(Qt::NoPen); + if (radius > 0.) { + const auto more = qCeil(radius * 2); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.fillRect(0, 0, more, more, Qt::transparent); + p.fillRect(rect.width() - more, 0, more, more, Qt::transparent); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.setBrush(gradient); + p.drawRoundedRect( + rect.marginsAdded({ 0, 0, 0, more }), + radius, + radius); + } else { + p.fillRect(rect, gradient); + } + + const auto lottieSize = st::creditsHistoryEntryStarGiftSize; + const auto paused = (expand == 0.); + const auto modelScale = expand + + ((st::giftBoxStickerTiny.width() / float64(lottieSize)) + * (1. - expand)); + const auto finalShift = (size.height() - lottieSize) / 2; + const auto shift = (1. - expand) * (finalShift - st::uniqueGiftModelTop); + p.translate(0, shift); + paintModel(p, _state->now.model, { + .width = size.width(), + }, modelScale, paused); + p.translate(0, -shift); + + const auto skip = anim::interpolate(size.width() / 3, 0, expand); + const auto pointsHeight = anim::interpolate( + context.initial.height(), + st::uniqueGiftSubtitleTop, + expand); + p.translate(-skip, 0); + paintPattern(p, _state->now.pattern, _state->now.backdrop, { + .width = size.width() + 2 * skip, + .patternAreaHeight = pointsHeight, + }, bgProgress); + + return QRect(QPoint(), size * ratio); +} + +bool UniqueGiftCoverWidget::paintGift( + QPainter &p, + GiftView &gift, + const PaintContext &context, + float64 shown) { + Expects(gift.gift.has_value()); + + paintBackdrop(p, gift.backdrop, context); + if (gift.gift->pattern.document != gift.gift->model.document) { + paintPattern(p, gift.pattern, gift.backdrop, context, shown); + } + const auto finished = paintModel(p, gift.model, context); + if (gift.gift->crafted) { + const auto padding = st::chatUniqueGiftBadgePadding; + auto badge = Info::PeerGifts::GiftBadge{ + .text = tr::lng_gift_crafted_tag(tr::now), + .bg1 = gift.gift->backdrop.edgeColor, + .bg2 = gift.gift->backdrop.patternColor, + .fg = gift.gift->backdrop.textColor, + }; + if (_state->craftedBadge.isNull() + || _state->craftedBadgeKey != badge) { + _state->craftedBadgeKey = badge; + _state->craftedBadge = Info::PeerGifts::ValidateRotatedBadge( + badge, padding, true); + } + p.drawImage(0, 0, _state->craftedBadge); + } + return finished; +} + +QColor UniqueGiftCoverWidget::backgroundColor() const { + return _state->released.bg; +} + +QColor UniqueGiftCoverWidget::foregroundColor() const { + return _state->released.fg; +} + +void UniqueGiftCoverWidget::paintSpinnerAnimation( + QPainter &p, + const PaintContext &context) { + using SpinnerState = Data::GiftUpgradeSpinner::State; + + const auto select = [&](auto &&list, int index, auto &fallback) + -> decltype(auto) { + return (index >= 0) ? list[index] : fallback; + }; + + const auto now = crl::now(); + const auto elapsed = now - _state->spinStarted; + auto current = _state->spinner->state.current(); + const auto stateTo = [&](SpinnerState to) { + if (int(current) < int(to)) { + _state->spinner->state = current = to; + } + }; + const auto switchTo = [&](SpinnerState to, crl::time at) { + if (elapsed >= at) { + stateTo(to); + } + }; + const auto actualize = [&]( + AttributeSpin &spin, + auto &&list, + SpinnerState checkState, + SpinnerState finishState, + int slowdown = 1) { + if (spin.progress() < 1.) { + return; + } else if (current >= checkState) { + spin.startToTarget([this] { update(); }, slowdown); + stateTo(finishState); + } else { + spin.startWithin(list.size(), [this] { update(); }); + } + }; + if (anim::Disabled() && current < SpinnerState::FinishedModel) { + stateTo(SpinnerState::FinishedModel); + _state->backdropSpin.startToTarget([this] { update(); }); + _state->patternSpin.startToTarget([this] { update(); }); + _state->modelSpin.startToTarget([this] { update(); }); + } + if (_state->backdropSpin.willIndex != 0) { + switchTo(SpinnerState::FinishBackdrop, kBackdropStopsAt); + } + actualize( + _state->backdropSpin, + _state->spinnerBackdrops, + SpinnerState::FinishBackdrop, + SpinnerState::FinishedBackdrop); + + if (current == SpinnerState::FinishedBackdrop + && _state->patternSpin.willIndex != 0) { + switchTo(SpinnerState::FinishPattern, kPatternStopsAt); + } + actualize( + _state->patternSpin, + _state->spinnerPatterns, + SpinnerState::FinishPattern, + SpinnerState::FinishedPattern); + + if (current == SpinnerState::FinishedPattern + && _state->modelSpin.willIndex != 0) { + switchTo(SpinnerState::FinishModel, kModelStopsAt); + } + actualize( + _state->modelSpin, + _state->spinnerModels, + SpinnerState::FinishModel, + SpinnerState::FinishedModel, + 2); + + auto &backdropNow = select( + _state->spinnerBackdrops, + _state->backdropSpin.nowIndex, + _state->now.backdrop); + auto &backdropWill = select( + _state->spinnerBackdrops, + _state->backdropSpin.willIndex, + _state->now.backdrop); + const auto backdropProgress = _state->backdropSpin.progress(); + _state->updateColorsFromBackdrops( + backdropNow, + backdropWill, + backdropProgress); + + auto &patternNow = select( + _state->spinnerPatterns, + _state->patternSpin.nowIndex, + _state->now.pattern); + auto &patternWill = select( + _state->spinnerPatterns, + _state->patternSpin.willIndex, + _state->now.pattern); + const auto patternProgress = _state->patternSpin.progress(); + const auto paintPatterns = [&]( + QPainter &p, + const BackdropView &backdrop) { + if (patternProgress >= 1.) { + paintPattern(p, patternWill, backdrop, context, 1.); + } else { + paintPattern(p, patternNow, backdrop, context, 1. - patternProgress); + if (patternProgress > 0.) { + paintPattern(p, patternWill, backdrop, context, patternProgress); + } + } + }; + + if (backdropProgress >= 1.) { + p.drawImage(0, 0, prepareBackdrop(backdropWill, context)); + paintPatterns(p, backdropWill); + } else { + p.drawImage(0, 0, prepareBackdrop(backdropNow, context)); + paintPatterns(p, backdropNow); + + const auto fade = context.width / 2; + const auto from = anim::interpolate( + -fade, + context.width, + backdropProgress); + if (const auto till = from + fade; till > 0) { + auto faded = prepareBackdrop(backdropWill, context); + auto q = QPainter(&faded); + paintPatterns(q, backdropWill); + + q.setCompositionMode( + QPainter::CompositionMode_DestinationIn); + auto brush = QLinearGradient(from, 0, till, 0); + brush.setStops({ + { 0., QColor(255, 255, 255, 255) }, + { 1., QColor(255, 255, 255, 0) }, + }); + const auto ratio = int(faded.devicePixelRatio()); + const auto imgHeight = faded.height() / ratio; + q.fillRect(from, 0, fade, imgHeight, brush); + q.end(); + + p.drawImage( + QRect(0, 0, till, imgHeight), + faded, + QRect(0, 0, till * ratio, faded.height())); + } + } + + auto &modelWas = select( + _state->spinnerModels, + _state->modelSpin.wasIndex, + _state->now.model); + auto &modelNow = select( + _state->spinnerModels, + _state->modelSpin.nowIndex, + _state->now.model); + auto &modelWill = select( + _state->spinnerModels, + _state->modelSpin.willIndex, + _state->now.model); + const auto modelProgress = _state->modelSpin.progress(); + const auto paintOneModel = [&](ModelView &view, float64 progress) { + if (progress >= 1. || progress <= -1.) { + return; + } + auto scale = 1.; + if (progress != 0.) { + const auto shift = progress * context.width / 2.; + const auto shown = 1. - std::abs(progress); + scale = kModelScaleFrom + shown * (1. - kModelScaleFrom); + p.save(); + p.setOpacity(shown); + p.translate(int(base::SafeRound(shift)), 0); + } + paintModel(p, view, context, scale, true); + if (progress != 0.) { + p.restore(); + } + }; + const auto initial = (_state->modelSpin.nowIndex < 0); + const auto ending = (current == SpinnerState::FinishedModel) + && !_state->modelSpin.willIndex; + const auto willProgress = ending + ? (modelProgress - 1.) + : (-1. + modelProgress * 2 / 3.); + const auto nowProgress = ending + ? (modelProgress * 4 / 3. - 1 / 3.) + : initial + ? (modelProgress * 1 / 3.) + : (willProgress + 2 / 3.); + const auto wasProgress = ending + ? std::min(nowProgress + 2 / 3., 1.) + : 1. + (modelProgress - 1.) * 2 / 3.; + if (!initial) { + paintOneModel(modelWas, wasProgress); + } + paintOneModel(modelNow, nowProgress); + paintOneModel(modelWill, willProgress); + + if (anim::Disabled() + || (ending + && modelProgress >= 1. + && backdropProgress >= 1. + && patternProgress >= 1.)) { + const auto take = [&](auto &&list) { + auto result = std::move(list.front()); + list.clear(); + return result; + }; + _state->now.gift = *_state->spinner->target; + _state->now.backdrop = take(_state->spinnerBackdrops); + _state->now.pattern = take(_state->spinnerPatterns); + _state->now.model = take(_state->spinnerModels); + _state->now.forced = true; + _state->spinStarted = 0; + _state->spinner->state = SpinnerState::Finished; + } +} + +void UniqueGiftCoverWidget::paintNormalAnimation( + QPainter &p, + const PaintContext &context, + float64 progress) { + if (progress < 1.) { + const auto finished = paintGift(p, _state->now, context, 1. - progress) + || (_state->next.forced + && (!_state->crossfading + || !_state->crossfade.animating())); + const auto next = finished + ? _state->next.model.lottie.get() + : nullptr; + if (next && next->ready()) { + _state->crossfading = true; + _state->updateAttributesPending = true; + _state->crossfade.start([this] { + update(); + }, 0., 1., kCrossfadeDuration); + } else if (finished) { + if (const auto onstack = _state->checkSpinnerStart) { + onstack(); + } + } + } + if (progress > 0.) { + p.setOpacity(progress); + paintGift(p, _state->next, context, progress); + } +} + +void GiftReleasedByHandler(not_null peer) { + const auto session = &peer->session(); + const auto window = session->tryResolveWindow(peer); + if (window) { + window->showPeerHistory(peer); + return; + } + const auto account = not_null(&session->account()); + if (const auto window = Core::App().windowFor(account)) { + window->invokeForSessionController( + &session->account(), + peer, + [=](not_null window) { + window->showPeerHistory(peer); + }); + } +} + +object_ptr MakeUniqueGiftCover( + QWidget *parent, + rpl::producer data, + UniqueGiftCoverArgs &&args) { + return object_ptr( + parent, + std::move(data), + std::move(args)); +} + +void AddUniqueGiftCover( + not_null container, + rpl::producer data, + UniqueGiftCoverArgs &&args) { + container->add( + MakeUniqueGiftCover(container, std::move(data), std::move(args))); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_cover_box.h b/Telegram/SourceFiles/boxes/star_gift_cover_box.h new file mode 100644 index 0000000000..4e425e073a --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_cover_box.h @@ -0,0 +1,128 @@ +/* +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 "data/data_star_gift.h" +#include "ui/rp_widget.h" + +namespace Data { +struct UniqueGift; +struct GiftUpgradeSpinner; +} // namespace Data + +namespace Ui { + +class RpWidget; +class VerticalLayout; + +struct UniqueGiftCoverArgs { + rpl::producer pretitle; + rpl::producer numberText; + rpl::producer subtitle; + Fn subtitleClick; + bool subtitleLinkColored = false; + bool subtitleOutlined = false; + rpl::producer resalePrice; + Fn resaleClick; + bool attributesInfo = false; + Fn now, + std::optional next, + float64 progress)> repaintedHook; + std::shared_ptr upgradeSpinner; +}; + +struct UniqueGiftCover { + Data::UniqueGift values; + bool spinner = false; + bool force = false; +}; + +void GiftReleasedByHandler(not_null peer); + +class UniqueGiftCoverWidget final : public RpWidget { +public: + UniqueGiftCoverWidget( + QWidget *parent, + rpl::producer data, + UniqueGiftCoverArgs &&args); + ~UniqueGiftCoverWidget(); + + struct PaintContext { + int width = 0; + int patternAreaHeight = 0; + }; + + struct CraftContext { + QSize initial; + QColor initialBg; + float64 fadeInProgress = 0.; + float64 expandProgress = 0.; + }; + [[nodiscard]] QRect prepareCraftFrame( + QImage &canvas, + const CraftContext &context); + + [[nodiscard]] QColor backgroundColor() const; + [[nodiscard]] QColor foregroundColor() const; + +private: + struct BackdropView; + struct PatternView; + struct ModelView; + struct GiftView; + + void paintEvent(QPaintEvent *e) override; + + [[nodiscard]] QImage prepareBackdrop( + BackdropView &backdrop, + const PaintContext &context); + void paintBackdrop( + QPainter &p, + BackdropView &backdrop, + const PaintContext &context); + void paintPattern( + QPainter &p, + PatternView &pattern, + const BackdropView &backdrop, + const PaintContext &context, + float64 shown); + bool paintModel( + QPainter &p, + ModelView &model, + const PaintContext &context, + float64 scale = 1., + bool paused = false); + bool paintGift( + QPainter &p, + GiftView &gift, + const PaintContext &context, + float64 shown); + + void paintSpinnerAnimation(QPainter &p, const PaintContext &context); + void paintNormalAnimation( + QPainter &p, + const PaintContext &context, + float64 progress); + + struct State; + const std::unique_ptr _state; + +}; + +[[nodiscard]] object_ptr MakeUniqueGiftCover( + QWidget *parent, + rpl::producer data, + UniqueGiftCoverArgs &&args); + +void AddUniqueGiftCover( + not_null container, + rpl::producer data, + UniqueGiftCoverArgs &&args); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp new file mode 100644 index 0000000000..e901ec135c --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp @@ -0,0 +1,1997 @@ +/* +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/star_gift_craft_animation.h" + +#include "base/call_delayed.h" +#include "base/unixtime.h" +#include "boxes/star_gift_box.h" +#include "boxes/star_gift_cover_box.h" +#include "boxes/star_gift_craft_box.h" +#include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/stickers_lottie.h" +#include "data/data_credits.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_session.h" +#include "history/view/media/history_view_sticker_player.h" +#include "info/peer_gifts/info_peer_gifts_common.h" +#include "lang/lang_keys.h" +#include "lang/lang_tag.h" +#include "lottie/lottie_common.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "settings/settings_credits_graphics.h" +#include "ui/boxes/boost_box.h" +#include "ui/boxes/confirm_box.h" +#include "ui/image/image_prepare.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/top_background_gradient.h" +#include "ui/painter.h" +#include "ui/ui_utility.h" +#include "styles/style_boxes.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" + +namespace Ui { +namespace { + +using namespace Info::PeerGifts; + +constexpr auto kFrameDuration = 1000. / 60.; +constexpr auto kFlightDuration = crl::time(400); +constexpr auto kLoadingFadeInDuration = crl::time(150); +constexpr auto kLoadingFadeOutDuration = crl::time(300); +constexpr auto kLoadingMinDuration = crl::time(600); +constexpr auto kFailureFadeDuration = crl::time(400); +constexpr auto kSuccessFadeInDuration = crl::time(300); +constexpr auto kSuccessExpandDuration = crl::time(400); +constexpr auto kSuccessExpandStart = crl::time(100); +constexpr auto kProgressFadeInDuration = crl::time(300); +constexpr auto kFailureFadeInDuration = crl::time(300); + +[[nodiscard]] QString FormatPercent(int permille) { + const auto rounded = (permille + 5) / 10; + return QString::number(rounded) + '%'; +} + +struct Rotation { + float64 x = 0.; + float64 y = 0.; + + Rotation operator+(Rotation other) const { + return { x + other.x, y + other.y }; + } + Rotation operator*(float64 scale) const { + return { x * scale, y * scale }; + } +}; + +using RotationFn = Fn; + +[[nodiscard]] int Slowing() { + return anim::SlowMultiplier(); +} + +[[nodiscard]] RotationFn DecayingRotation(Rotation impulse, float64 decay) { + const auto lambda = -std::log(decay) / kFrameDuration; + return [=](Rotation initial, crl::time t) { + const auto factor = (1. - std::exp(-lambda * t)) / lambda / 1000.; + return initial + impulse * factor; + }; +} + +[[nodiscard]] RotationFn DecayingEndRotation( + Rotation impulse, + float64 decay, + Rotation target, + crl::time duration) { + const auto lambda = -std::log(decay) / kFrameDuration; + target = target * M_PI; + return [=](Rotation initial, crl::time t) { + const auto factor = (1. - std::exp(-lambda * t)) / lambda / 1000.; + const auto result = initial + impulse * factor; + if (t <= duration - 200) { + return result; + } else if (t >= duration) { + return target; + } + const auto progress = (duration - t) / 200.; + return result * progress + target * (1. - progress); + }; +} + +struct GiftAnimationConfig { + RotationFn rotation; + crl::time duration = 0; + crl::time revealTime = 0; + int targetFaceIndex = 0; + int targetFaceRotation = 0; +}; + +const auto kGiftAnimations = std::array{{ { + .rotation = DecayingEndRotation({ 4.1, -8.2 }, 0.98, { 1, -2 }, 2200), + .duration = 2200, + .revealTime = 600, + .targetFaceIndex = 1, + .targetFaceRotation = 180, +}, { + .rotation = DecayingRotation({ 3.2, -5.9 }, 0.97), + .duration = 1200, + .revealTime = 0, + .targetFaceIndex = 4, + .targetFaceRotation = 180, +}, { + .rotation = DecayingEndRotation({ 6.2, 7.8 }, 0.98, { 2, 1 }, 2200), + .duration = 2200, + .revealTime = 1000, + .targetFaceIndex = 1, + .targetFaceRotation = 0, +}, { + .rotation = DecayingRotation({ 7.0, 4.5 }, 0.97), + .duration = 1200, + .revealTime = 200, + .targetFaceIndex = 5, + .targetFaceRotation = 0, +}, { + .rotation = DecayingEndRotation({ -6.5, 5.0 }, 0.98, { 0, 1 }, 2200), + .duration = 2200, + .revealTime = 800, + .targetFaceIndex = 1, + .targetFaceRotation = 0, +}, { + .rotation = DecayingRotation({ -2.5, 3.5 }, 0.98), + .duration = 1200, + .revealTime = 200, + .targetFaceIndex = 2, + .targetFaceRotation = 180, +}, { + .rotation = DecayingEndRotation({ -4.4, -8.2 }, 0.98, { 0, -1.5 }, 2200), + .duration = 2200, + .revealTime = 600, + .targetFaceIndex = 3, + .targetFaceRotation = 0, +} }}; + +[[nodiscard]] QImage CreateBgGradient( + QSize size, + const Data::UniqueGiftBackdrop &backdrop) { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + auto gradient = QRadialGradient( + QPoint(size.width() / 2, size.height() / 2), + size.height() / 2); + gradient.setStops({ + { 0., backdrop.centerColor }, + { 1., backdrop.edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRect(0, 0, size.width(), size.height()); + p.end(); + + const auto mask = Images::CornersMask(st::boxRadius); + return Images::Round(std::move(result), mask); +} + +[[nodiscard]] std::optional> ComputeCubeFaceCorners( + QPointF center, + float64 size, + float64 rotationX, + float64 rotationY, + int faceIndex) { + struct Vec3 { + float64 x, y, z; + + Vec3 operator+(const Vec3 &o) const { + return { x + o.x, y + o.y, z + o.z }; + } + Vec3 operator-(const Vec3 &o) const { + return { x - o.x, y - o.y, z - o.z }; + } + Vec3 operator*(float64 s) const { + return { x * s, y * s, z * s }; + } + [[nodiscard]] Vec3 cross(const Vec3 &o) const { + return { + y * o.z - z * o.y, + z * o.x - x * o.z, + x * o.y - y * o.x + }; + } + [[nodiscard]] float64 dot(const Vec3 &o) const { + return x * o.x + y * o.y + z * o.z; + } + [[nodiscard]] Vec3 normalized() const { + const auto len = std::sqrt(x * x + y * y + z * z); + return (len > 0.) ? Vec3{ x / len, y / len, z / len } : *this; + } + }; + + const auto rotateY = [](Vec3 v, float64 angle) { + const auto c = std::cos(angle); + const auto s = std::sin(angle); + return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c }; + }; + const auto rotateX = [](Vec3 v, float64 angle) { + const auto c = std::cos(angle); + const auto s = std::sin(angle); + return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c }; + }; + + const auto half = size / 2.; + constexpr auto kFocalLength = 800.; + + struct FaceDefinition { + std::array corners; + Vec3 normal; + }; + + const auto faces = std::array{{ + {{{{ -half, -half, -half }, + { half, -half, -half }, + { half, half, -half }, + { -half, half, -half }}}, + { 0., 0., -1. }}, + + {{{{ half, -half, half }, + { -half, -half, half }, + { -half, half, half }, + { half, half, half }}}, + { 0., 0., 1. }}, + + {{{{ -half, -half, half }, + { -half, -half, -half }, + { -half, half, -half }, + { -half, half, half }}}, + { -1., 0., 0. }}, + + {{{{ half, -half, -half }, + { half, -half, half }, + { half, half, half }, + { half, half, -half }}}, + { 1., 0., 0. }}, + + {{{{ -half, -half, half }, + { half, -half, half }, + { half, -half, -half }, + { -half, -half, -half }}}, + { 0., -1., 0. }}, + + {{{{ -half, half, -half }, + { half, half, -half }, + { half, half, half }, + { -half, half, half }}}, + { 0., 1., 0. }}, + }}; + + if (faceIndex < 0 || faceIndex >= 6) { + return std::nullopt; + } + + const auto &face = faces[faceIndex]; + + const auto transformedNormal = rotateX( + rotateY(face.normal, rotationY), + rotationX); + if (transformedNormal.z >= 0.) { + return std::nullopt; + } + + auto result = std::array(); + for (auto i = 0; i != 4; ++i) { + const auto p = rotateX( + rotateY(face.corners[i], rotationY), + rotationX); + + const auto viewZ = p.z + half + kFocalLength; + if (viewZ <= 0.) { + return std::nullopt; + } + + const auto scale = kFocalLength / viewZ; + result[i] = QPointF( + center.x() + p.x * scale, + center.y() + p.y * scale); + } + + return result; +} + +void PaintCubeFace( + QPainter &p, + const std::array &corners, + const QImage &face, + QSize faceSize = QSize(), + const QRect &source = QRect(), + int rotation = 0) { + if (face.isNull()) { + return; + } + + const auto ratio = style::DevicePixelRatio(); + const auto origin = source.isEmpty() ? face.size() : source.size(); + if (faceSize.isEmpty()) { + faceSize = origin / ratio; + } + const auto w = faceSize.width(); + const auto h = faceSize.height(); + + const auto srcRect = QPolygonF({ + QPointF(0, 0), + QPointF(w, 0), + QPointF(w, h), + QPointF(0, h) + }); + + const auto rotationSteps = (rotation / 90) % 4; + auto rotatedCorners = corners; + for (auto i = 0; i < rotationSteps; ++i) { + rotatedCorners = { + rotatedCorners[1], + rotatedCorners[2], + rotatedCorners[3], + rotatedCorners[0], + }; + } + + const auto dstRect = QPolygonF({ + rotatedCorners[0], + rotatedCorners[1], + rotatedCorners[2], + rotatedCorners[3] + }); + + auto transform = QTransform(); + if (!QTransform::quadToQuad(srcRect, dstRect, transform)) { + return; + } + + PainterHighQualityEnabler hq(p); + p.save(); + p.setTransform(transform, true); + const auto delta = source.isEmpty() + ? QPointF() + : QPointF( + ((origin.width() / ratio) - faceSize.width()) / 2., + ((origin.height() / ratio) - faceSize.height()) / 2.); + p.drawImage(QRectF(-delta, QSizeF(origin) / ratio), face, source); + p.restore(); +} + +[[nodiscard]] std::vector GetVisibleCubeFaces( + float64 rotationX, + float64 rotationY) { + struct Vec3 { + float64 x, y, z; + + Vec3 operator*(float64 s) const { + return { x * s, y * s, z * s }; + } + }; + + const auto rotateY = [](Vec3 v, float64 angle) { + const auto c = std::cos(angle); + const auto s = std::sin(angle); + return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c }; + }; + const auto rotateX = [](Vec3 v, float64 angle) { + const auto c = std::cos(angle); + const auto s = std::sin(angle); + return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c }; + }; + + constexpr auto normals = std::array{{ + { 0., 0., -1. }, + { 0., 0., 1. }, + { -1., 0., 0. }, + { 1., 0., 0. }, + { 0., -1., 0. }, + { 0., 1., 0. }, + }}; + + struct FaceDepth { + int index; + float64 z; + }; + auto visible = std::vector(); + visible.reserve(3); + + for (auto i = 0; i != 6; ++i) { + const auto transformed = rotateX( + rotateY(normals[i], rotationY), + rotationX); + if (transformed.z < 0.) { + visible.push_back({ i, transformed.z }); + } + } + + ranges::sort(visible, ranges::greater(), &FaceDepth::z); + + auto result = std::vector(); + result.reserve(visible.size()); + for (const auto &face : visible) { + result.push_back(face.index); + } + return result; +} + +void PaintCubeFirstFlight( + QPainter &p, + not_null state, + float64 progress, + bool skipForgeIcon = false) { + const auto shared = state->shared.get(); + + const auto overlayBg = shared->forgeBgOverlay; + auto sideBg = shared->forgeBg1; + sideBg.setAlphaF(progress); + + auto hq = PainterHighQualityEnabler(p); + const auto forge = shared->forgeRect; + const auto radius = (1. - progress) * st::boxRadius; + p.setPen(Qt::NoPen); + p.setBrush(overlayBg); + p.drawRoundedRect(forge, radius, radius); + p.setBrush(sideBg); + p.drawRoundedRect(forge, radius, radius); + + if (!skipForgeIcon) { + st::craftForge.paintInCenter(p, forge, st::white->c); + } +} + +std::unique_ptr SetupFailureAnimation( + not_null canvas, + not_null state) { + auto result = std::make_unique(); + result->lottie = Lottie::MakeIcon({ + .name = u"craft_failed"_q, + .sizeOverride = st::craftFailLottieSize, + }); + return result; +} + +void FailureAnimationCheck( + not_null state, + not_null canvas, + crl::time now) { + const auto animation = state->failureAnimation.get(); + if (!animation || animation->started) { + return; + } + const auto elapsed = (now - state->animationStartTime); + const auto left = (state->animationDuration * Slowing()) - elapsed; + if (left <= 0 || left > kFailureFadeDuration * Slowing()) { + return; + } + animation->started = true; + auto &covers = state->shared->covers; + for (auto i = 0; i != 5; ++i) { + auto &cover = covers[i]; + if (!cover.shown) { + animation->finalCoverIndex = i; + if (i != 4) { + auto &last = covers.back(); + cover.backdrop = last.backdrop; + cover.pattern = std::move(last.pattern); + cover.button1 = last.button1; + cover.button2 = last.button2; + } + cover.shown = true; + cover.shownAnimation.start([=] { + canvas->update(); + }, 0., 1., left); + break; + } + } + + const auto lottie = animation->lottie.get(); + if (lottie->framesCount() > 1) { + lottie->animate([=] { + canvas->update(); + }, 0, lottie->framesCount() - 1); + } + + state->failureShown = true; + state->progressShown = false; +} + +void FailureAnimationPrepareFrame( + not_null state, + not_null canvas, + int faceIndex) { + if (!state->failureAnimation) { + state->failureAnimation = SetupFailureAnimation( + canvas, + state); + } + const auto shared = state->shared.get(); + const auto animation = state->failureAnimation.get(); + if (animation->frame.isNull()) { + animation->frame = shared->forgeSides[faceIndex].frame; + } + const auto lastIndex = animation->finalCoverIndex; + const auto progress = (lastIndex >= 0) + ? shared->covers[lastIndex].shownAnimation.value(1.) + : 0.; + const auto bg = anim::color( + shared->forgeSides[faceIndex].bg, + shared->forgeFail, + progress); + const auto radius = progress * st::boxRadius; + const auto rounded = (radius > 0.); + if (rounded) { + animation->frame.fill(Qt::transparent); + } + + auto p = QPainter(&animation->frame); + const auto size = shared->forgeRect.size(); + const auto rect = QRect(QPoint(), size); + if (rounded) { + auto hq = PainterHighQualityEnabler(p); + p.setBrush(bg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(rect, radius, radius); + } else { + p.fillRect(rect, bg); + } + animation->lottie->paintInCenter(p, rect); + p.setOpacity(progress); + p.drawImage( + st::craftForgePadding, + st::craftForgePadding, + state->shared->lostRadial); +} + +[[nodiscard]] QRect PrepareCraftFrame( + not_null state, + int faceIndex, + crl::time now) { + Expects(state->successAnimation != nullptr); + + const auto &shared = state->shared.get(); + const auto success = state->successAnimation.get(); + const auto elapsed = now - state->animationStartTime; + const auto end = state->animationDuration * Slowing(); + const auto left = end - elapsed; + const auto fadeDuration = kSuccessFadeInDuration * Slowing(); + const auto fadeInProgress = std::clamp( + (1. - (left / float64(fadeDuration))), + 0., + 1.); + const auto expandStart = kSuccessExpandStart * Slowing(); + const auto expanding = kSuccessExpandDuration * Slowing(); + const auto expandProgress = std::clamp( + (expandStart - left) / float64(expanding), + 0., + 1.); + if (expandProgress == 1. && !success->finished) { + success->finished = true; + } + const auto expanded = anim::easeOutCubic(1., expandProgress); + success->expanded = expanded; + return success->widget->prepareCraftFrame(success->frame, { + .initial = shared->forgeRect.size(), + .initialBg = shared->forgeSides[faceIndex].bg, + .fadeInProgress = fadeInProgress, + .expandProgress = expanded, + }); +} + +void PaintCube( + QPainter &p, + not_null state, + not_null canvas, + QPointF center, + float64 size) { + const auto shared = state->shared.get(); + + const auto now = crl::now(); + FailureAnimationCheck(state, canvas, now); + + const auto success = state->successAnimation.get(); + const auto failure = state->failureAnimation.get(); + + const auto finalCoverIndex = failure ? failure->finalCoverIndex : -1; + const auto finishingFailure = (finalCoverIndex >= 0) + ? shared->covers[finalCoverIndex].shownAnimation.value(1.) + : 0.; + + if (!state->nextFaceRevealed + && state->currentPhaseIndex >= 0) { + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = (crl::now() - state->animationStartTime); + if (elapsed >= config.revealTime * Slowing()) { + state->nextFaceRevealed = true; + } + } + + const auto faces = GetVisibleCubeFaces( + state->rotationX, + state->rotationY); + + auto paintFailure = Fn(); + for (const auto faceIndex : faces) { + const auto corners = ComputeCubeFaceCorners( + center, + size, + state->rotationX, + state->rotationY, + faceIndex); + if (!corners) { + continue; + } + + const auto thisFaceRevealed = state->nextFaceRevealed + && (faceIndex == state->nextFaceIndex); + auto faceSize = shared->forgeRect.size(); + auto faceImage = shared->forgeSides[faceIndex].frame; + auto faceSource = QRect(); + auto faceRotation = thisFaceRevealed + ? state->nextFaceRotation + : 0; + + for (auto i = 0; i < 4; ++i) { + if (i != state->currentlyFlying + && state->giftToSide[i].face == faceIndex + && shared->corners[i].giftButton) { + faceSize = QSize(); // Take from the frame. + faceImage = shared->corners[i].gift(1.); + faceRotation = state->giftToSide[i].rotation; + break; + } + } + + if (thisFaceRevealed && state->allGiftsLanded) { + const auto cover = success ? success->widget : nullptr; + if (cover) { + faceSource = PrepareCraftFrame(state, faceIndex, now); + faceImage = success->frame; + } else { + FailureAnimationPrepareFrame( + state, + canvas, + faceIndex); + faceImage = state->failureAnimation->frame; + } + } + if (finishingFailure > 0.) { + p.setOpacity((faceIndex != state->nextFaceIndex) + ? (1. - finishingFailure) + : 1.); + } + PaintCubeFace( + p, + *corners, + faceImage, + faceSize, + faceSource, + faceRotation); + } + if (finishingFailure > 0.) { + p.setOpacity(1.); + } +} + +struct GiftFlightPosition { + QPointF origin; + float64 scale; +}; + +[[nodiscard]] GiftFlightPosition ComputeGiftFlightPosition( + QRect originalRect, + QPointF targetCenter, + float64 targetSize, + float64 progress, + float64 startOffsetY) { + const auto eased = progress; + + const auto startOrigin = QPointF(originalRect.topLeft()) + + QPointF(0, startOffsetY); + const auto targetOrigin = targetCenter + - QPointF(targetSize, targetSize) / 2.; + const auto currentOrigin = startOrigin + + (targetOrigin - startOrigin) * eased; + + const auto originalSize = std::max( + originalRect.width(), + originalRect.height()); + const auto endScale = (originalSize > 0.) + ? (targetSize / float64(originalSize)) + : 1.; + const auto currentScale = 1. + (endScale - 1.) * eased; + + return { + .origin = currentOrigin, + .scale = currentScale, + }; +} + +[[nodiscard]] FacePlacement GetCameraFacingFreeFace( + not_null state) { + struct Vec3 { + float64 x, y, z; + }; + + const auto rotateY = [](Vec3 v, float64 angle) { + const auto c = std::cos(angle); + const auto s = std::sin(angle); + return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c }; + }; + const auto rotateX = [](Vec3 v, float64 angle) { + const auto c = std::cos(angle); + const auto s = std::sin(angle); + return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c }; + }; + + constexpr auto normals = std::array{{ + { 0., 0., -1. }, + { 0., 0., 1. }, + { -1., 0., 0. }, + { 1., 0., 0. }, + { 0., -1., 0. }, + { 0., 1., 0. }, + }}; + + constexpr auto faceUpVectors = std::array{{ + { 0., -1., 0. }, + { 0., -1., 0. }, + { 0., -1., 0. }, + { 0., -1., 0. }, + { 0., 0., -1. }, + { 0., 0., 1. }, + }}; + + const auto isOccupied = [&](int faceIndex) { + for (const auto &placement : state->giftToSide) { + if (placement.face == faceIndex) { + return true; + } + } + return false; + }; + + auto bestFace = -1; + auto bestZ = 0.; + + for (auto i = 0; i != 6; ++i) { + if (isOccupied(i)) { + continue; + } + const auto transformed = rotateX( + rotateY(normals[i], state->rotationY), + state->rotationX); + if (bestFace < 0 || transformed.z < bestZ) { + bestFace = i; + bestZ = transformed.z; + } + } + + if (bestFace < 0) { + return {}; + } + + const auto faceUp = rotateX( + rotateY(faceUpVectors[bestFace], state->rotationY), + state->rotationX); + + const auto screenUpY = faceUp.y; + const auto screenUpX = faceUp.x; + + auto rotation = 0; + if (std::abs(screenUpY) >= std::abs(screenUpX)) { + rotation = (screenUpY < 0.) ? 0 : 180; + } else { + rotation = (screenUpX < 0.) ? 90 : 270; + } + + return { .face = bestFace, .rotation = rotation }; +} + +[[nodiscard]] std::array InterpolateQuadCorners( + const std::array &from, + const std::array &to, + float64 progress) { + return { + from[0] + (to[0] - from[0]) * progress, + from[1] + (to[1] - from[1]) * progress, + from[2] + (to[2] - from[2]) * progress, + from[3] + (to[3] - from[3]) * progress, + }; +} + +[[nodiscard]] std::array RectToCorners(QRectF rect) { + return { + rect.topLeft(), + rect.topRight(), + rect.bottomRight(), + rect.bottomLeft(), + }; +} + +void PaintFlyingGiftWithQuad( + QPainter &p, + const CraftState::CornerSnapshot &corner, + const std::array &corners, + float64 progress) { + if (!corner.giftButton) { + return; + } + + const auto frame = corner.gift(progress); + PaintCubeFace(p, corners, frame); +} + +void PaintFlyingGift( + QPainter &p, + const CraftState::CornerSnapshot &corner, + const GiftFlightPosition &position, + float64 progress) { + if (!corner.giftButton) { + return; + } + + const auto ratio = style::DevicePixelRatio(); + const auto frame = corner.gift(progress); + const auto giftSize = QSizeF(frame.size()) / ratio; + const auto scaledSize = giftSize * position.scale; + const auto giftTopLeft = position.origin; + + auto hq = PainterHighQualityEnabler(p); + p.drawImage(QRectF(giftTopLeft, scaledSize), frame); +} + +void PaintSlideOutCorner( + QPainter &p, + const CraftState::CornerSnapshot &corner, + float64 progress) { + if (corner.originalRect.isEmpty()) { + return; + } + + const auto ratio = style::DevicePixelRatio(); + const auto currentTopLeft = QPointF(corner.originalRect.topLeft()); + if (corner.giftButton) { + const auto frame = corner.gift(0.); + const auto giftSize = QSizeF(frame.size()) / ratio; + const auto giftTopLeft = currentTopLeft; + + const auto &extend = st::defaultDropdownMenu.wrap.shadow.extend; + const auto innerTopLeft = giftTopLeft + + QPointF(extend.left(), extend.top()); + const auto innerWidth = giftSize.width() + - extend.left() + - extend.right(); + + p.drawImage(giftTopLeft, frame); + + const auto badgeFade = std::clamp(1. - progress / 0.7, 0., 1.); + if (!corner.percentBadge.isNull() && badgeFade > 0.) { + const auto badgeSize = QSizeF(corner.percentBadge.size()) + / ratio; + const auto badgePos = innerTopLeft + - QPointF(badgeSize.width() / 3., badgeSize.height() / 3.); + p.setOpacity(badgeFade); + p.drawImage(badgePos, corner.percentBadge); + p.setOpacity(1.); + } + + if (!corner.removeButton.isNull() && badgeFade > 0.) { + const auto removeSize = QSizeF(corner.removeButton.size()) + / ratio; + const auto removePos = innerTopLeft + + QPointF( + (innerWidth + - removeSize.width() + + removeSize.width() / 3.), + -removeSize.height() / 3.); + p.setOpacity(badgeFade); + p.drawImage(removePos, corner.removeButton); + p.setOpacity(1.); + } + } else if (!corner.addButton.isNull()) { + const auto fadeProgress = std::clamp(progress * 1.5, 0., 1.); + const auto addTopLeft = currentTopLeft; + p.setOpacity(1. - fadeProgress); + p.drawImage(addTopLeft, corner.addButton); + p.setOpacity(1.); + } +} + +void PaintSlideOutPhase( + QPainter &p, + not_null shared, + QSize canvasSize, + float64 progress) { + const auto ratio = style::DevicePixelRatio(); + + if (!shared->bottomPart.isNull()) { + const auto slideDistance = QSizeF(shared->bottomPart.size()).height() + / ratio; + const auto offsetY = slideDistance * progress; + const auto opacity = 1. - progress; + + p.setOpacity(opacity); + p.drawImage( + QPointF(0, shared->bottomPartY + offsetY), + shared->bottomPart); + p.setOpacity(1.); + } + + for (const auto &corner : shared->corners) { + PaintSlideOutCorner(p, corner, progress); + } + + auto hq = PainterHighQualityEnabler(p); + const auto forge = shared->forgeRect; + const auto radius = st::boxRadius; + p.setPen(Qt::NoPen); + p.setBrush(shared->forgeBgOverlay); + p.drawRoundedRect(forge, radius, radius); + st::craftForge.paintInCenter(p, forge, st::white->c); + p.setOpacity(1. - progress); + p.drawImage( + forge.x() + st::craftForgePadding, + forge.y() + st::craftForgePadding, + shared->forgePercent); +} + +void PaintFailureThumbnails( + QPainter &p, + not_null shared, + QSize canvasSize, + float64 fadeProgress) { + if (fadeProgress <= 0. || shared->lostGifts.empty()) { + return; + } + + p.setOpacity(fadeProgress); + + const auto width = canvasSize.width(); + const auto giftsCount = int(shared->lostGifts.size()); + const auto &firstCorner = shared->corners[ + shared->lostGifts.front().cornerIndex]; + if (!firstCorner.giftButton) { + p.setOpacity(1.); + return; + } + const auto thumbSize = firstCorner.giftButton->size(); + const auto &extend = st::defaultDropdownMenu.wrap.shadow.extend; + const auto thumbSpacing = st::boxRowPadding.left() / 2; + const auto totalThumbWidth = giftsCount * thumbSize.width() + + (giftsCount - 1) * thumbSpacing; + const auto available = width - extend.left() - extend.right(); + const auto skip = (totalThumbWidth > available) + ? (available - giftsCount * thumbSize.width()) / (giftsCount - 1) + : thumbSpacing; + const auto full = giftsCount * thumbSize.width() + + (giftsCount - 1) * skip; + auto x = (width - full) / 2; + const auto y = shared->forgeRect.bottom() + st::craftFailureThumbsTop; + const auto rubberOut = st::lineWidth; + + for (const auto &gift : shared->lostGifts) { + if (gift.cornerIndex < 0) { + x += thumbSize.width() + skip; + continue; + } + const auto &corner = shared->corners[gift.cornerIndex]; + const auto giftFrame = corner.gift(0.); + if (!giftFrame.isNull()) { + const auto targetRect = QRect(QPoint(x, y), thumbSize); + p.drawImage(targetRect, giftFrame); + + if (!gift.number.isEmpty()) { + if (gift.badgeCache.isNull()) { + const auto burnedBg = BurnedBadgeBg(); + gift.badgeCache = ValidateRotatedBadge(GiftBadge{ + .text = gift.number, + .bg1 = burnedBg, + .bg2 = burnedBg, + .fg = st::white->c, + .small = true, + }, QMargins()); + } + const auto inner = targetRect.marginsRemoved(extend); + p.save(); + p.setClipRect(inner.marginsAdded( + { rubberOut, rubberOut, rubberOut, rubberOut })); + const auto badgeW = gift.badgeCache.width() + / gift.badgeCache.devicePixelRatio(); + p.drawImage( + inner.x() + inner.width() + rubberOut - badgeW, + inner.y() - rubberOut, + gift.badgeCache); + p.restore(); + } + } + x += thumbSize.width() + skip; + } + + p.setOpacity(1.); +} + +[[nodiscard]] std::array RotateCornersForFace( + std::array corners, + int rotation) { + const auto steps = (rotation / 90) % 4; + for (auto i = 0; i < steps; ++i) { + corners = { + corners[1], + corners[2], + corners[3], + corners[0], + }; + } + return corners; +} + +void StartGiftFlight( + not_null state, + not_null canvas, + int startIndex) { + const auto &corners = state->shared->corners; + + auto nextGift = -1; + for (auto i = startIndex; i < 4; ++i) { + if (corners[i].giftButton) { + nextGift = i; + break; + } + } + + if (nextGift < 0) { + return; + } + + state->currentlyFlying = nextGift; + state->giftToSide[nextGift] = GetCameraFacingFreeFace(state); + state->flightTargetCorners = std::nullopt; + + state->flightAnimation.start( + [=] { canvas->update(); }, + 0., + 1., + kFlightDuration, + anim::easeInCubic); + + if (!state->continuousAnimation.animating()) { + state->continuousAnimation.start(); + } +} + +void StartGiftFlightToFace( + not_null state, + not_null canvas, + int targetFace) { + const auto shared = state->shared.get(); + const auto &corners = shared->corners; + + auto nextCorner = -1; + for (auto i = 0; i < 4; ++i) { + if (corners[i].giftButton && state->giftToSide[i].face < 0) { + nextCorner = i; + break; + } + } + + if (nextCorner < 0) { + return; + } + + state->currentlyFlying = nextCorner; + + const auto faceRotation = state->nextFaceRotation; + state->giftToSide[nextCorner] = FacePlacement{ + .face = targetFace, + .rotation = faceRotation, + }; + + if (state->currentPhaseIndex >= 0) { + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto initial = Rotation{ + state->initialRotationX, + state->initialRotationY, + }; + const auto endRotation = config.rotation(initial, config.duration); + + const auto cubeSize = float64(shared->forgeRect.width()); + const auto cubeCenter = QPointF(shared->forgeRect.topLeft()) + + QPointF(cubeSize, cubeSize) / 2.; + + const auto targetCorners = ComputeCubeFaceCorners( + cubeCenter, + cubeSize, + endRotation.x, + endRotation.y, + targetFace); + if (targetCorners) { + state->flightTargetCorners = RotateCornersForFace( + *targetCorners, + faceRotation); + } else { + state->flightTargetCorners = std::nullopt; + } + } else { + state->flightTargetCorners = std::nullopt; + } + + state->flightAnimation.start([=] { + canvas->update(); + }, 0., 1., kFlightDuration, anim::easeInCubic); + + if (!state->continuousAnimation.animating()) { + state->continuousAnimation.start(); + } +} + +void LandCurrentGift(not_null state, crl::time now) { + if (state->currentlyFlying < 0) { + return; + } + + const auto giftNumber = ++state->giftsLanded; + const auto isLastGift + = state->allGiftsLanded + = (giftNumber >= state->totalGifts); + + const auto configIndex = (giftNumber - 1) * 2 + (isLastGift ? 0 : 1); + Assert(configIndex < 7); + + const auto &config = kGiftAnimations[configIndex]; + + state->currentlyFlying = -1; + + state->initialRotationX = state->rotationX; + state->initialRotationY = state->rotationY; + state->currentPhaseIndex = configIndex; + state->currentPhaseFinished = false; + state->animationStartTime = now; + state->animationDuration = config.duration; + state->nextFaceIndex = config.targetFaceIndex; + state->nextFaceRotation = config.targetFaceRotation; + state->nextFaceRevealed = false; +} + +void PaintLoadingAnimation( + QPainter &p, + not_null state) { + const auto &loading = state->loadingAnimation; + if (!loading || !state->loadingStartedTime) { + return; + } + + const auto loadingShown = state->loadingShownAnimation.value( + state->loadingFadingOut ? 0. : 1.); + if (loadingShown <= 0.) { + return; + } + + const auto shared = state->shared.get(); + const auto forge = shared->forgeRect; + const auto inner = forge.marginsRemoved({ + st::craftForgePadding, + st::craftForgePadding, + st::craftForgePadding, + st::craftForgePadding, + }); + + const auto radial = loading->computeState(); + const auto adjustedState = RadialState{ + .shown = radial.shown * loadingShown, + .arcFrom = radial.arcFrom, + .arcLength = radial.arcLength, + }; + + auto pen = QPen(st::white->c); + pen.setCapStyle(Qt::RoundCap); + InfiniteRadialAnimation::Draw( + p, + adjustedState, + inner.topLeft(), + inner.size(), + inner.width(), + pen, + st::craftForgeLoading.thickness); +} + +} // namespace + +QImage CraftState::CornerSnapshot::gift(float64 progress) const { + if (!giftButton) { + return QImage(); + } else if (progress == 1. && giftFrameFinal) { + return giftFrame; + } else if (progress < 1.) { + giftFrameFinal = false; + } + if (giftButton->makeCraftFrameIsFinal(giftFrame, progress)) { + giftFrameFinal = true; + } + return giftFrame; +} + +void CraftState::paint( + QPainter &p, + QSize size, + int craftingHeight, + float64 slideProgress) { + const auto width = size.width(); + const auto getBackdrop = [&](BackdropView &backdrop) { + const auto ratio = style::DevicePixelRatio(); + const auto gradientSize = size; + auto &gradient = backdrop.gradient; + if (gradient.size() != gradientSize * ratio) { + gradient = CreateBgGradient(gradientSize, backdrop.colors); + } + return gradient; + }; + auto patternOffsetY = 0.; + const auto paintPattern = [&]( + QPainter &q, + PatternView &pattern, + const BackdropView &backdrop, + float64 shown) { + const auto color = backdrop.colors.patternColor; + const auto key = (color.red() << 16) + | (color.green() << 8) + | color.blue(); + if (patternOffsetY != 0.) { + q.translate(0, patternOffsetY); + } + PaintBgPoints( + q, + PatternBgPoints(), + pattern.emojis[key], + pattern.emoji.get(), + color, + QRect(0, 0, width, st::boxTitleHeight + craftingHeight), + shown); + if (patternOffsetY != 0.) { + q.translate(0, -patternOffsetY); + } + }; + auto animating = false; + auto newEdgeColor = std::optional(); + auto newButton1 = QColor(); + auto newButton2 = QColor(); + for (auto i = 0; i != 5; ++i) { + auto &cover = covers[i]; + if (cover.shownAnimation.animating()) { + animating = true; + } + const auto finalValue = cover.shown ? 1. : 0.; + const auto shown = cover.shownAnimation.value(finalValue); + if (shown <= 0.) { + break; + } else if (shown == 1.) { + const auto next = i + 1; + if (next < 4 + && covers[next].shown + && !covers[next].shownAnimation.animating()) { + continue; + } + } + p.setOpacity(shown); + p.drawImage(0, 0, getBackdrop(cover.backdrop)); + paintPattern(p, cover.pattern, cover.backdrop, 1.); + if (coversAnimate) { + const auto edge = cover.backdrop.colors.edgeColor; + if (!newEdgeColor) { + newEdgeColor = edge; + newButton1 = cover.button1; + newButton2 = cover.button2; + } else { + newEdgeColor = anim::color(*newEdgeColor, edge, shown); + newButton1 = anim::color(newButton1, cover.button1, shown); + newButton2 = anim::color(newButton2, cover.button2, shown); + } + } + } + if (newEdgeColor) { + edgeColor = *newEdgeColor; + button1 = newButton1; + button2 = newButton2; + } + if (animating) { + p.setOpacity(1.); + } else { + coversAnimate = false; + } +} + +void CraftState::updateForGiftCount(int count, Fn repaint) { + for (auto i = 5; i != 0;) { + auto &cover = covers[--i]; + const auto shown = (i < count); + if (cover.shown != shown) { + cover.shown = shown; + const auto from = shown ? 0. : 1.; + const auto to = shown ? 1. : 0.; + cover.shownAnimation.start( + repaint, + from, + to, + st::fadeWrapDuration); + coversAnimate = true; + } + } +} + +CraftState::EmptySide CraftState::prepareEmptySide(int index) const { + const auto size = forgeRect.size(); + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + + const auto bg = anim::color(forgeBg1, forgeBg2, index / 5.); + result.fill(bg); + + auto p = QPainter(&result); + st::craftForge.paintInCenter(p, QRect(QPoint(), size), st::white->c); + +//#if _DEBUG +// p.setFont(st::semiboldFont); +// p.setPen(st::white); +// p.drawText( +// size.width() / 10, +// size.width() / 10 + st::semiboldFont->ascent, +// QString::number(index)); +//#endif // _DEBUG + + p.end(); + + return { .bg = bg, .frame = result }; +} + +void SetupCraftProgressTitle( + not_null container, + not_null*> opacity) { + const auto row = container->add( + object_ptr(container), + st::boxRowPadding + st::craftProgressTitleMargin, + style::al_top); + + const auto label = CreateChild( + row, + tr::lng_gift_craft_progress(), + st::uniqueGiftTitle); + label->setTextColorOverride(st::white->c); + + const auto iconSize = st::craftProgressIconSize; + const auto iconWidget = CreateChild(row); + iconWidget->resize(iconSize); + + auto owned = Lottie::MakeIcon({ + .name = u"craft_progress"_q, + .sizeOverride = iconSize, + .limitFps = true, + }); + const auto icon = owned.get(); + iconWidget->lifetime().add([kept = std::move(owned)] {}); + + const auto startAnimation = [=] { + icon->animate( + [=] { iconWidget->update(); }, + 0, + icon->framesCount() - 1); + }; + startAnimation(); + + iconWidget->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(iconWidget); + p.setOpacity(opacity->current()); + icon->paint(p, 0, 0); + if (!icon->animating() && icon->frameIndex() > 0) { + startAnimation(); + } + }, iconWidget->lifetime()); + + rpl::combine( + label->sizeValue(), + row->sizeValue() + ) | rpl::on_next([=](QSize labelSize, QSize rowSize) { + const auto skip = st::craftProgressIconSkip; + const auto totalWidth = iconSize.width() + skip + labelSize.width(); + const auto rowHeight = std::max(iconSize.height(), labelSize.height()); + if (rowSize.height() != rowHeight) { + row->resize(rowSize.width(), rowHeight); + return; + } + const auto left = (rowSize.width() - totalWidth) / 2; + iconWidget->move(left, (rowHeight - iconSize.height()) / 2); + label->move( + left + iconSize.width() + skip, + (rowHeight - labelSize.height()) / 2); + }, row->lifetime()); + + opacity->value( + ) | rpl::on_next([=](float64 value) { + label->setOpacity(value); + iconWidget->update(); + }, row->lifetime()); + +} + +void SetupProgressControls( + not_null state, + not_null canvas) { + const auto add = [&](not_null label) { + state->progressOpacity.value() | rpl::on_next([=](float64 opacity) { + label->setOpacity(opacity); + }, label->lifetime()); + }; + + const auto controls = CreateChild(canvas); + controls->resizeToWidth(canvas->width()); + controls->move(0, state->shared->craftingBottom); + + SetupCraftProgressTitle(controls, &state->progressOpacity); + + const auto subColor = QColor(255, 255, 255, 178); + const auto about = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress_about( + lt_gift, + rpl::single(tr::marked(state->shared->giftName)), + tr::rich), + st::uniqueGiftSubtitle), + st::boxRowPadding + st::craftProgressAboutMargin, + style::al_top); + add(about); + about->setTextColorOverride(subColor); + + const auto warning = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress_fails(), + st::craftAbout), + st::boxRowPadding + st::craftProgressWarningMargin, + style::al_top); + add(warning); + warning->setTextColorOverride(subColor); + + const auto chance = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress_chance( + tr::now, + lt_percent, + FormatPercent(state->shared->successPermille)), + st::craftChance), + st::boxRowPadding + st::craftProgressChanceMargin, + style::al_top); + add(chance); + chance->setTextColorOverride(st::white->c); + chance->paintOn([=](QPainter &p) { + if (const auto opacity = state->progressOpacity.current()) { + p.setPen(Qt::NoPen); + p.setOpacity(opacity); + p.setBrush(state->shared->forgeBgOverlay); + const auto radius = chance->height() / 2.; + p.drawRoundedRect(chance->rect(), radius, radius); + } + }); + + controls->showOn(state->progressOpacity.value( + ) | rpl::map( + rpl::mappers::_1 > 0. + ) | rpl::distinct_until_changed()); +} + +void SetupFailureControls( + not_null state, + not_null canvas) { + const auto add = [&](not_null label) { + state->failureOpacity.value() | rpl::on_next([=](float64 opacity) { + label->setOpacity(opacity); + }, label->lifetime()); + }; + + const auto controls = CreateChild(canvas); + controls->resizeToWidth(canvas->width()); + controls->move(0, state->shared->craftingBottom); + + const auto title = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_failed_title(), + st::uniqueGiftTitle), + st::boxRowPadding + st::craftFailureTitleMargin, + style::al_top); + add(title); + title->setTextColorOverride(QColor(0xF8, 0x4A, 0x4A)); + + const auto about = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_failed_about( + lt_count, + rpl::single(state->shared->lostGifts.size() * 1.), + tr::rich), + st::uniqueGiftSubtitle), + st::boxRowPadding + st::craftFailureAboutMargin, + style::al_top); + add(about); + about->setTextColorOverride(QColor(0xFF, 0xBC, 0x9B)); + + controls->showOn(state->failureOpacity.value( + ) | rpl::map( + rpl::mappers::_1 > 0. + ) | rpl::distinct_until_changed()); +} + +void SetupCraftNewButton( + not_null state, + not_null canvas) { + const auto button = CreateChild( + canvas, + QGradientStops{ + { 0., QColor(0xE2, 0x75, 0x19) }, + { 1., QColor(0xDD, 0x48, 0x19) }, + }); + button->setFullRadius(true); + button->setClickedCallback([=] { + state->retryWithNewGift(); + }); + + const auto buttonLabel = CreateChild( + button, + tr::lng_gift_craft_new_button(), + st::creditsBoxButtonLabel); + buttonLabel->setTextColorOverride(st::white->c); + buttonLabel->setAttribute( + Qt::WA_TransparentForMouseEvents); + button->sizeValue() | rpl::on_next([=](QSize size) { + buttonLabel->moveToLeft( + (size.width() - buttonLabel->width()) / 2, + st::giftBox.button.textTop); + }, buttonLabel->lifetime()); + + const auto buttonWidth = canvas->width() + - st::craftBoxButtonPadding.left() + - st::craftBoxButtonPadding.right(); + const auto buttonHeight = st::giftBox.button.height; + button->setNaturalWidth(buttonWidth); + button->setGeometry( + st::craftBoxButtonPadding.left(), + state->shared->originalButtonY, + buttonWidth, + buttonHeight); + button->show(); + + button->shownValue() | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::on_next([=] { + button->startGlareAnimation(); + }, button->lifetime()); +} + +void StartCraftAnimation( + not_null box, + std::shared_ptr show, + std::shared_ptr shared, + Fn callback)> startRequest, + Fn closeCurrent)> retryWithNewGift) { + const auto container = box->verticalLayout(); + while (container->count() > 0) { + delete container->widgetAt(0); + } + + auto canvas = object_ptr(container); + const auto raw = canvas.data(); + const auto state = raw->lifetime().make_state(); + + state->retryWithNewGift = [=] { + retryWithNewGift(crl::guard(box, [=] { box->closeBox(); })); + }; + + const auto title = CreateChild( + raw, + tr::lng_gift_craft_title(), + st::uniqueGiftTitle); + title->naturalWidthValue() | rpl::on_next([=](int titleWidth) { + const auto width = st::boxWideWidth; + title->moveToLeft( + (width - titleWidth) / 2, + st::craftTitleMargin.top(), + width); + }, title->lifetime()); + title->setTextColorOverride(st::white->c); + + const auto height = shared->containerHeight; + const auto craftingHeight = shared->craftingBottom - shared->craftingTop; + state->shared = std::move(shared); + state->shared->craftingStarted = true; + + const auto craftingSize = QSize(container->width(), height); + raw->resize(craftingSize); + + for (auto &corner : state->shared->corners) { + if (corner.giftButton) { + ++state->totalGifts; + } + } + + state->loadingAnimation = std::make_unique( + [=] { raw->update(); }, + st::craftForgeLoading); + + SetupProgressControls(state, raw); + SetupFailureControls(state, raw); + + state->progressShown.value( + ) | rpl::combine_previous( + ) | rpl::on_next([=](bool wasShown, bool nowShown) { + if (wasShown == nowShown) { + return; + } + const auto from = wasShown ? 1. : 0.; + const auto to = nowShown ? 1. : 0.; + state->progressFadeIn.start([=] { + raw->update(); + }, from, to, kProgressFadeInDuration, anim::easeOutCubic); + }, raw->lifetime()); + + state->failureShown.value( + ) | rpl::combine_previous( + ) | rpl::on_next([=](bool wasShown, bool nowShown) { + if (wasShown == nowShown) { + return; + } + const auto from = wasShown ? 1. : 0.; + const auto to = nowShown ? 1. : 0.; + state->failureFadeIn.start([=] { + raw->update(); + if (!state->failureFadeIn.animating()) { + SetupCraftNewButton(state, raw); + } + }, from, to, kFailureFadeDuration, anim::easeOutCubic); + }, raw->lifetime()); + + raw->paintOn([=](QPainter &p) { + const auto shared = state->shared.get(); + const auto success = state->successAnimation.get(); + const auto failure = state->failureAnimation.get(); + const auto slideProgress = state->slideOutAnimation.value(1.); + if (slideProgress < 1. || (failure && failure->started)) { + shared->paint(p, craftingSize, craftingHeight, slideProgress); + } else { + if (shared->craftBg.isNull()) { + const auto ratio = style::DevicePixelRatio(); + shared->craftBg = QImage( + craftingSize * ratio, + QImage::Format_ARGB32_Premultiplied); + shared->craftBg.setDevicePixelRatio(ratio); + shared->craftBg.fill(Qt::transparent); + + auto q = QPainter(&shared->craftBg); + shared->paint(q, craftingSize, craftingHeight, 1.); + } + if (success && success->hiding) { + p.setOpacity(1. - success->heightAnimation.value(1.)); + } + p.drawImage(0, 0, shared->craftBg); + if (success && success->hiding) { + p.setOpacity(1.); + } + } + + if (slideProgress < 1.) { + PaintSlideOutPhase(p, shared, craftingSize, slideProgress); + } else { + const auto craftingOffsetY = success + ? (-success->coverShift * success->expanded) + : 0.; + if (success && success->expanded > 0.) { + const auto width = st::boxWideWidth; + const auto top = st::craftTitleMargin.top(); + title->moveToLeft( + (width - title->naturalWidth()) / 2, + top - (success->expanded * (top + title->height())), + width); + } + const auto cubeSize = float64(shared->forgeRect.width()); + const auto cubeCenter = QPointF(shared->forgeRect.topLeft()) + + QPointF(cubeSize, cubeSize) / 2. + + QPointF(0, craftingOffsetY); + + for (auto i = 0; i < 4; ++i) { + const auto &corner = shared->corners[i]; + if (corner.giftButton && state->giftToSide[i].face < 0) { + const auto giftTopLeft = QPointF() + + QPointF(corner.originalRect.topLeft()) + + QPointF(0, craftingOffsetY); + p.drawImage(giftTopLeft, corner.gift(0)); + } + } + + const auto flying = state->currentlyFlying; + const auto firstFlyProgress = (flying == 0) + ? state->flightAnimation.value(1.) + : state->giftsLanded + ? 1. + : 0.; + const auto loadingShown = state->loadingStartedTime + && !state->loadingFadingOut; + if (firstFlyProgress < 1.) { + const auto skipForgeIcon = loadingShown && anim::Disabled(); + PaintCubeFirstFlight( + p, + state, + firstFlyProgress, + skipForgeIcon); + } else { + PaintCube(p, state, raw, cubeCenter, cubeSize); + } + + if (flying >= 0) { + const auto &corner = shared->corners[flying]; + const auto progress = (flying > 0) + ? state->flightAnimation.value(1.) + : firstFlyProgress; + + if (state->flightTargetCorners) { + const auto sourceRect = QRectF(corner.originalRect) + .translated(0, craftingOffsetY); + const auto sourceCorners = RectToCorners(sourceRect); + const auto interpolatedCorners = InterpolateQuadCorners( + sourceCorners, + *state->flightTargetCorners, + progress); + PaintFlyingGiftWithQuad( + p, + corner, + interpolatedCorners, + progress); + } else { + const auto position = ComputeGiftFlightPosition( + corner.originalRect, + cubeCenter, + cubeSize, + progress, + craftingOffsetY); + PaintFlyingGift(p, corner, position, progress); + } + } + + PaintLoadingAnimation(p, state); + + if (const auto opacity = state->failureOpacity.current()) { + PaintFailureThumbnails( + p, + shared, + craftingSize, + opacity); + } + } + + state->progressOpacity = state->progressFadeIn.value( + state->progressShown.current() ? 1. : 0.); + state->failureOpacity = state->failureFadeIn.value( + state->failureShown.current() ? 1. : 0.); + }); + + const auto startFlying = [=] { + if (state->loadingStartedTime > 0) { + state->loadingFadingOut = true; + state->loadingShownAnimation.start( + [=] { raw->update(); }, + 1., + 0., + kLoadingFadeOutDuration); + } + StartGiftFlight(state, raw, 0); + }; + + const auto tryStartFlying = [=] { + const auto slideFinished = !state->slideOutAnimation.animating(); + const auto resultArrived = state->craftResult.has_value(); + const auto notYetFlying = (state->currentlyFlying < 0) + && (state->giftsLanded == 0); + if (!slideFinished || !notYetFlying) { + return; + } + if (!resultArrived) { + if (!state->loadingStartedTime) { + state->loadingStartedTime = crl::now(); + state->loadingAnimation->start(); + state->loadingShownAnimation.start( + [=] { raw->update(); }, + 0., + 1., + kLoadingFadeInDuration); + } + return; + } + if (!state->loadingStartedTime) { + startFlying(); + return; + } + const auto elapsed = crl::now() - state->loadingStartedTime; + if (elapsed >= kLoadingMinDuration) { + startFlying(); + } else { + base::call_delayed( + kLoadingMinDuration - elapsed, + raw, + startFlying); + } + }; + + state->progressShown = true; + + state->slideOutAnimation.start([=] { + raw->update(); + + const auto progress = state->slideOutAnimation.value(1.); + if (progress >= 1.) { + tryStartFlying(); + } + }, 0., 1., crl::time(300), anim::easeOutCubic); + + state->continuousAnimation.init([=](crl::time now) { + if (state->currentPhaseIndex >= 0) { + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = state->currentPhaseFinished + ? (config.duration * Slowing()) + : (now - state->animationStartTime); + const auto initial = Rotation{ + state->initialRotationX, + state->initialRotationY, + }; + const auto r = config.rotation(initial, elapsed / Slowing()); + state->rotationX = r.x; + state->rotationY = r.y; + } + + raw->update(); + + if (state->currentlyFlying >= 0 + && !state->flightAnimation.animating()) { + LandCurrentGift(state, now); + } + + if (!state->flightAnimation.animating() + && state->currentlyFlying < 0 + && state->giftsLanded > 0 + && state->giftsLanded < state->totalGifts + && state->currentPhaseIndex >= 0) { + const auto elapsed = now - state->animationStartTime; + const auto start = state->animationDuration - kFlightDuration; + if (elapsed >= start * Slowing()) { + StartGiftFlightToFace(state, raw, state->nextFaceIndex); + } + } + + const auto success = state->successAnimation.get(); + if (!state->currentPhaseFinished + && !state->flightAnimation.animating() + && state->currentlyFlying < 0 + && state->giftsLanded >= state->totalGifts + && state->totalGifts > 0 + && state->currentPhaseIndex >= 0) { + const auto elapsed = now - state->animationStartTime; + if (elapsed >= state->animationDuration * Slowing()) { + state->currentPhaseFinished = true; + if (success) { + const auto wasContent = box->height(); + const auto wasTotal = box->parentWidget()->height(); + const auto wasButtons = wasTotal - wasContent; + + container->clear(); + container->add(std::move(success->owned)); + + box->setStyle(st::giftBox); + + auto craftAnotherCallback = crl::guard(box, [=] { + retryWithNewGift([=] { box->closeBox(); }); + }); + + const auto entry = EntryForUpgradedGift( + *state->craftResult, + 0, + nullptr, + craftAnotherCallback); + Settings::GenericCreditsEntryBody(box, show, entry); + container->resizeToWidth(st::boxWideWidth); + + const auto nowContent = box->height(); + const auto nowTotal = box->parentWidget()->height(); + const auto nowButtons = nowTotal - nowContent; + + box->animateHeightFrom( + wasContent + wasButtons - nowButtons); + success->hiding = true; + const auto duration = kSuccessExpandDuration + - kSuccessExpandStart; + success->heightAnimation.start([=] { + const auto height = anim::interpolate( + craftingSize.height(), + success->coverHeight, + success->heightAnimation.value(1.)); + raw->resize(craftingSize.width(), height); + }, 0., 1., duration, anim::easeOutCubic); + + raw->setParent(box->parentWidget()); + raw->show(); + } + } + } + if (success && success->finished) { + state->continuousAnimation.stop(); + raw->hide(); + + StartFireworks(box->parentWidget()); + } + return true; + }); + + container->add(std::move(canvas)); + + if (startRequest) { + const auto weak = base::make_weak(raw); + startRequest([=](CraftResult result) { + if (!weak) { + return; + } else if (v::is(result)) { + box->uiShow()->show(MakeInformBox({ + .text = v::get(result).type, + })); + box->closeBox(); + return; + } else if (v::is(result)) { + const auto when = base::unixtime::now() + + v::get(result).seconds; + ShowCraftLaterError(box->uiShow(), when); + box->closeBox(); + return; + } + using Result = std::shared_ptr; + state->craftResult = v::get(result); + if (const auto result = state->craftResult->get()) { + auto numberText = (result->info.unique && result->info.unique->number > 0) + ? rpl::single(u"#"_q + Lang::FormatCountDecimal(result->info.unique->number)) + : rpl::producer(); + + state->successAnimation = std::make_unique< + CraftDoneAnimation + >(CraftDoneAnimation{ + .owned = MakeUniqueGiftCover( + container, + rpl::single(UniqueGiftCover{ *result->info.unique }), + { .numberText = std::move(numberText) }), + }); + const auto success = state->successAnimation.get(); + + const auto raw = success->owned.get(); + success->widget = raw; + raw->hide(); + raw->resizeToWidth(st::boxWideWidth); + SendPendingMoveResizeEvents(raw); + + success->coverHeight = raw->height(); + success->coverShift = state->shared->forgeRect.y() + + (state->shared->forgeRect.height() / 2) + - (raw->height() / 2); + } + tryStartFlying(); + }); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h new file mode 100644 index 0000000000..c1a1aa58e1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h @@ -0,0 +1,223 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" +#include "base/unique_qptr.h" +#include "data/data_star_gift.h" +#include "ui/effects/animations.h" +#include "ui/effects/radial_animation.h" +#include "lottie/lottie_icon.h" +#include "ui/text/text_custom_emoji.h" + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Main { +class Session; +} // namespace Main + +namespace HistoryView { +class StickerPlayer; +} // namespace HistoryView + +namespace Info::PeerGifts { +class GiftButton; +} // namespace Info::PeerGifts + +namespace Ui { + +class FlatLabel; +class GenericBox; +class RpWidget; +class VerticalLayout; +class UniqueGiftCoverWidget; + +struct CraftResultError { + QString type; +}; + +struct CraftResultWait { + TimeId seconds = 0; +}; + +using CraftResult = std::variant< + std::shared_ptr, + CraftResultError, + CraftResultWait>; + +struct BackdropView { + Data::UniqueGiftBackdrop colors; + QImage gradient; +}; + +struct PatternView { + std::unique_ptr emoji; + base::flat_map> emojis; +}; + +struct CraftState { + struct Cover { + BackdropView backdrop; + PatternView pattern; + QColor button1; + QColor button2; + Animations::Simple shownAnimation; + bool shown = false; + }; + + std::array covers; // Last one for failed background. + rpl::variable edgeColor; + QColor button1; + QColor button2; + bool coversAnimate = false; + bool craftingStarted = false; + QImage craftBg; + + QImage topPart; + QRect topPartRect; + QImage bottomPart; + int bottomPartY = 0; + + struct CornerSnapshot { + base::unique_qptr giftButton; + mutable QImage giftFrame; + QImage percentBadge; + QImage removeButton; + QImage addButton; + QRect originalRect; + mutable bool giftFrameFinal = false; + + [[nodiscard]] QImage gift(float64 progress) const; + }; + std::array corners; + + QRect forgeRect; + + QColor forgeBgOverlay; + QColor forgeBg1; + QColor forgeBg2; + QColor forgeFail; + QImage forgePercent; + + struct EmptySide { + QColor bg; + QImage frame; + }; + std::array forgeSides; + + Main::Session *session = nullptr; + + int containerHeight = 0; + int craftingTop = 0; + int craftingBottom = 0; + int craftingAreaCenterY = 0; + int originalButtonY = 0; + + QString giftName; + int successPermille = 0; + struct LostGift { + int cornerIndex = -1; + QString number; + mutable QImage badgeCache; + }; + std::vector lostGifts; + QImage lostRadial; + + void paint( + QPainter &p, + QSize size, + int craftingHeight, + float64 slideProgress = 0.); + void updateForGiftCount(int count, Fn repaint); + [[nodiscard]] EmptySide prepareEmptySide(int index) const; +}; + +struct FacePlacement { + int face = -1; + int rotation = 0; +}; + +struct CraftDoneAnimation { + object_ptr owned; + UniqueGiftCoverWidget *widget = nullptr; + Animations::Simple shownAnimation; + Animations::Simple heightAnimation; + QImage frame; + int coverHeight = 0; + int coverShift = 0; + float64 expanded = 0.; + bool finished = false; + bool hiding = false; +}; + +struct CraftFailAnimation { + QImage frame; + std::unique_ptr lottie; + bool started = false; + int finalCoverIndex = -1; + rpl::lifetime lifetime; +}; + +struct CraftAnimationState { + std::shared_ptr shared; + + Animations::Simple slideOutAnimation; + Animations::Basic continuousAnimation; + + float64 rotationX = 0.; + float64 rotationY = 0.; + + int currentlyFlying = -1; + int giftsLanded = 0; + int totalGifts = 0; + bool allGiftsLanded = false; + bool currentPhaseFinished = false; + std::array giftToSide; + Animations::Simple flightAnimation; + + int currentPhaseIndex = -1; + crl::time animationStartTime = 0; + crl::time animationDuration = 0; + float64 initialRotationX = 0.; + float64 initialRotationY = 0.; + int nextFaceIndex = 0; + int nextFaceRotation = 0; + bool nextFaceRevealed = false; + + std::optional> flightTargetCorners; + + std::optional> craftResult; + std::unique_ptr loadingAnimation; + Animations::Simple loadingShownAnimation; + crl::time loadingStartedTime = 0; + bool loadingFadingOut = false; + + std::unique_ptr successAnimation; + std::unique_ptr failureAnimation; + + rpl::variable progressOpacity; + Animations::Simple progressFadeIn; + rpl::variable progressShown; + + rpl::variable failureOpacity; + Animations::Simple failureFadeIn; + rpl::variable failureShown; + + Fn retryWithNewGift; +}; + +void StartCraftAnimation( + not_null box, + std::shared_ptr show, + std::shared_ptr state, + Fn callback)> startRequest, + Fn closeCurrent)> retryWithNewGift); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp new file mode 100644 index 0000000000..e8ecc0e922 --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp @@ -0,0 +1,1738 @@ +/* +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/star_gift_craft_box.h" + +#include "base/call_delayed.h" +#include "base/event_filter.h" +#include "base/random.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "boxes/star_gift_auction_box.h" +#include "boxes/star_gift_craft_animation.h" +#include "boxes/star_gift_preview_box.h" +#include "boxes/star_gift_resale_box.h" +#include "apiwrap.h" +#include "api/api_credits.h" +#include "api/api_premium.h" +#include "boxes/star_gift_box.h" +#include "data/components/gift_auctions.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "data/data_star_gift.h" +#include "data/data_user.h" +#include "info/peer_gifts/info_peer_gifts_common.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "ui/boxes/about_cocoon_box.h" // AddUniqueCloseButton. +#include "ui/boxes/confirm_box.h" +#include "ui/controls/button_labels.h" +#include "ui/controls/feature_list.h" +#include "ui/effects/numbers_animation.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/widgets/tooltip.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" +#include "ui/top_background_gradient.h" +#include "ui/ui_utility.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" // stickerPanDeleteIconFg +#include "styles/style_credits.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" + +namespace Ui { +namespace { + +using namespace Info::PeerGifts; + +struct ColorScheme { + Data::UniqueGiftBackdrop backdrop; + QColor button1; + QColor button2; +}; + +[[nodiscard]] QColor ForgeBgOverlay() { + return QColor(0xBA, 0xDF, 0xFF, 32); +} + +[[nodiscard]] QColor ForgeFailOverlay() { + return QColor(0xE1, 0x79, 0x23, 41); +} + +[[nodiscard]] std::array CraftBackdrops() { + struct Colors { + int center = 0; + int edge = 0; + int pattern = 0; + int button1 = 0; + int button2 = 0; + }; + const auto hardcoded = [](Colors colors) { + auto result = ColorScheme(); + const auto color = [](int value) { + return QColor( + (uint32(value) >> 16) & 0xFF, + (uint32(value) >> 8) & 0xFF, + (uint32(value)) & 0xFF); + }; + result.backdrop.centerColor = color(colors.center); + result.backdrop.edgeColor = color(colors.edge); + result.backdrop.patternColor = color(colors.pattern); + result.button1 = color(colors.button1); + result.button2 = color(colors.button2); + return result; + }; + return { + hardcoded({ 0x2C4359, 0x232E3F, 0x040C1A, 0x10A5DF, 0x2091E9 }), + hardcoded({ 0x2C4359, 0x232E3F, 0x040C1A, 0x10A5DF, 0x2091E9 }), + hardcoded({ 0x2C4359, 0x232E3F, 0x040C1A, 0x10A5DF, 0x2091E9 }), + hardcoded({ 0x1C4843, 0x1A2E37, 0x040C1A, 0x3ACA49, 0x007D9E }), + hardcoded({ 0x5D2E16, 0x371B1A, 0x040C1A, 0xE27519, 0xDD4819 }), + }; +} + +struct GiftForCraft { + std::shared_ptr unique; + Data::SavedStarGiftId manageId; + + [[nodiscard]] QString slugId() const { + return unique ? unique->slug : QString(); + } + + explicit operator bool() const { + return unique != nullptr; + } + friend inline bool operator==( + const GiftForCraft &, + const GiftForCraft &) = default; +}; + +struct CraftingView { + object_ptr widget; + rpl::producer editRequests; + rpl::producer removeRequests; + Fn)> grabForAnimation; +}; + +void ShowGiftCraftBox( + not_null controller, + std::vector gifts, + bool autoStartCraft); + +[[nodiscard]] QString FormatPercent(int permille) { + const auto rounded = (permille + 5) / 10; + return QString::number(rounded) + '%'; +} + +[[nodiscard]] not_null MakeRadialPercent( + not_null parent, + const style::CraftRadialPercent &st, + rpl::producer permille, + Fn format = FormatPercent) { + auto raw = CreateChild(parent); + + struct State { + State(const style::CraftRadialPercent &st, Fn callback) + : numbers(st.font, std::move(callback)) { + numbers.setDisabledMonospace(true); + } + + Animations::Simple animation; + NumbersAnimation numbers; + int permille = -1; + }; + const auto state = raw->lifetime().make_state( + st, + [=] { raw->update(); }); + + std::move(permille) | rpl::on_next([=](int value) { + if (state->permille == value) { + return; + } + state->animation.start([=] { + raw->update(); + }, state->permille, value, st::slideWrapDuration); + state->permille = value; + state->numbers.setText(format(value), value); + }, raw->lifetime()); + state->animation.stop(); + state->numbers.finishAnimating(); + + raw->show(); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + raw->paintOn([=, &st](QPainter &p) { + static constexpr auto kArcSkip = arc::kFullLength / 4; + static constexpr auto kArcStart = -(arc::kHalfLength - kArcSkip) / 2; + static constexpr auto kArcLength = arc::kFullLength - kArcSkip; + + const auto paint = [&](QColor color, float64 permille) { + p.setPen(QPen(color, st.stroke, Qt::SolidLine, Qt::RoundCap)); + p.setBrush(Qt::NoBrush); + const auto part = kArcLength * (permille / 1000.); + const auto length = int(base::SafeRound(part)); + const auto inner = raw->rect().marginsRemoved( + { st.stroke, st.stroke, st.stroke, st.stroke }); + p.drawArc(inner, kArcStart + kArcLength - length, length); + }; + + auto hq = PainterHighQualityEnabler(p); + + auto inactive = QColor(255, 255, 255, 64); + paint(inactive, 1000.); + paint(st::white->c, state->animation.value(state->permille)); + + state->numbers.paint( + p, + (raw->width() - state->numbers.countWidth()) / 2, + raw->height() - st.font->height, + raw->width()); + }); + + return raw; +} + +AbstractButton *MakeCornerButton( + not_null parent, + not_null button, + object_ptr content, + style::align align, + const GiftForCraft &gift, + rpl::producer edgeColor) { + Expects(content != nullptr); + + const auto result = CreateChild(parent); + result->show(); + + const auto inner = content.release(); + inner->setParent(result); + inner->show(); + inner->sizeValue() | rpl::on_next([=](QSize size) { + result->resize(size); + }, result->lifetime()); + inner->move(0, 0); + + rpl::combine( + button->geometryValue(), + result->sizeValue() + ) | rpl::on_next([=](QRect geometry, QSize size) { + const auto extend = st::defaultDropdownMenu.wrap.shadow.extend; + geometry = geometry.marginsRemoved(extend); + const auto out = QPoint(size.width(), size.height()) / 3; + const auto left = (align == style::al_left) + ? (geometry.x() - out.x()) + : (geometry.x() + geometry.width() - size.width() + out.x()); + const auto top = geometry.y() - out.y(); + result->move(left, top); + }, result->lifetime()); + + struct State { + rpl::variable edgeColor; + QColor buttonEdgeColor; + }; + const auto state = result->lifetime().make_state(); + state->edgeColor = std::move(edgeColor); + state->buttonEdgeColor = gift.unique->backdrop.edgeColor; + result->paintOn([=](QPainter &p) { + const auto right = result->width(); + const auto bottom = result->height(); + const auto add = QPoint(right, bottom) / 3; + const auto radius = bottom / 2.; + auto gradient = QLinearGradient( + (align == style::al_left) ? -add.x() : (right + add.x()), + -add.y(), + (align == style::al_left) ? (right + add.x()) : -add.x(), + bottom + add.y()); + gradient.setColorAt(0, state->edgeColor.current()); + gradient.setColorAt(1, state->buttonEdgeColor); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(gradient); + p.drawRoundedRect(result->rect(), radius, radius); + }); + + return result; +} + +AbstractButton *MakePercentButton( + not_null parent, + not_null button, + const GiftForCraft &gift, + rpl::producer edgeColor) { + auto label = object_ptr( + parent, + FormatPercent(gift.unique->craftChancePermille), + st::craftPercentLabel); + label->setTextColorOverride(st::white->c); + const auto result = MakeCornerButton( + parent, + button, + std::move(label), + style::al_left, + gift, + std::move(edgeColor)); + result->setAttribute(Qt::WA_TransparentForMouseEvents); + return result; +} + +AbstractButton *MakeRemoveButton( + not_null parent, + not_null button, + int size, + const GiftForCraft &gift, + Fn onClick, + rpl::producer edgeColor) { + auto remove = object_ptr(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; + icon.paint(p, add, add, add * 2 + icon.width(), st::white->c); + }); + remove->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto result = MakeCornerButton( + parent, + button, + std::move(remove), + style::al_right, + gift, + std::move(edgeColor)); + result->setClickedCallback(std::move(onClick)); + return result; +} + +[[nodiscard]] CraftingView MakeCraftingView( + not_null parent, + not_null session, + rpl::producer> chosen, + rpl::producer edgeColor) { + const auto width = st::boxWideWidth; + + const auto buttonPadding = st::craftPreviewPadding; + const auto buttonSize = st::giftBoxGiftTiny; + const auto height = 2 + * (buttonPadding.top() + buttonSize + buttonPadding.bottom()); + + auto widget = object_ptr(parent, height); + const auto raw = widget.data(); + + struct Entry { + GiftForCraft gift; + GiftButton *button = nullptr; + AbstractButton *add = nullptr; + AbstractButton *percent = nullptr; + AbstractButton *remove = nullptr; + }; + struct State { + explicit State(not_null session) + : delegate(session, GiftButtonMode::CraftPreview) { + } + + Delegate delegate; + std::array entries; + Fn refreshButton; + rpl::event_stream editRequests; + rpl::event_stream removeRequests; + rpl::variable chancePermille; + rpl::variable edgeColor; + RpWidget *forgeRadial = nullptr; + }; + const auto state = parent->lifetime().make_state(session); + state->edgeColor = std::move(edgeColor); + + state->refreshButton = [=](int index) { + Expects(index >= 0 && index < state->entries.size()); + + auto &entry = state->entries[index]; + const auto single = state->delegate.buttonSize(); + const auto geometry = QRect( + ((index % 2) + ? (width - buttonPadding.left() - single.width()) + : buttonPadding.left()), + ((index < 2) + ? buttonPadding.top() + : (height - buttonPadding.top() - single.height())), + single.width(), + single.height()); + delete base::take(entry.add); + delete base::take(entry.button); + delete base::take(entry.percent); + delete base::take(entry.remove); + + if (entry.gift) { + entry.button = CreateChild(raw, &state->delegate); + entry.button->setDescriptor(GiftTypeStars{ + .info = { + .id = entry.gift.unique->initialGiftId, + .unique = entry.gift.unique, + .document = entry.gift.unique->model.document, + }, + }, 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->setGeometry( + geometry, + state->delegate.buttonExtend()); + + entry.percent = MakePercentButton( + raw, + entry.button, + entry.gift, + state->edgeColor.value()); + } else { + entry.add = CreateChild(raw); + entry.add->show(); + entry.add->paintOn([=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + const auto radius = st::boxRadius; + p.setPen(Qt::NoPen); + p.setBrush(ForgeBgOverlay()); + + const auto rect = QRect(QPoint(), geometry.size()); + p.drawRoundedRect(rect, radius, radius); + + const auto &icon = st::craftAddIcon; + icon.paintInCenter(p, rect, st::white->c); + }); + entry.add->setClickedCallback([=] { + state->editRequests.fire_copy(index); + }); + entry.add->setGeometry(geometry); + } + + const auto count = 4 - ranges::count( + state->entries, + nullptr, + &Entry::button); + const auto canRemove = (count > 1); + 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) { + entry.remove = MakeRemoveButton( + raw, + entry.button, + entry.percent->height(), + entry.gift, + [=] { state->removeRequests.fire_copy(i); }, + state->edgeColor.value()); + } + } + } + }; + + std::move( + chosen + ) | rpl::on_next([=](const std::vector &gifts) { + auto chance = 0; + for (auto i = 0; i != 4; ++i) { + auto &entry = state->entries[i]; + const auto gift = (i < gifts.size()) ? gifts[i] : GiftForCraft(); + chance += gift.unique ? gift.unique->craftChancePermille : 0; + if (entry.gift == gift && (entry.button || entry.add)) { + continue; + } + entry.gift = gift; + state->refreshButton(i); + } + state->chancePermille = chance; + }, raw->lifetime()); + + const auto center = [&] { + const auto buttonPadding = st::craftPreviewPadding; + const auto buttonSize = st::giftBoxGiftTiny; + const auto left = buttonPadding.left() + + buttonSize + + buttonPadding.right(); + const auto center = (width - 2 * left); + const auto top = (height - center) / 2; + return QRect(left, top, center, center); + }(); + raw->paintOn([=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + + const auto radius = st::boxRadius; + + p.setPen(Qt::NoPen); + p.setBrush(ForgeBgOverlay()); + + p.drawRoundedRect(center, radius, radius); + + st::craftForge.paintInCenter(p, center, st::white->c); + }); + + state->forgeRadial = MakeRadialPercent( + raw, + st::craftForgePercent, + state->chancePermille.value()); + state->forgeRadial->setGeometry(center.marginsRemoved({ + st::craftForgePadding, + st::craftForgePadding, + st::craftForgePadding, + st::craftForgePadding, + })); + + auto grabForAnimation = [=](std::shared_ptr craftState) { + craftState->forgeRect = center; + craftState->forgePercent = GrabWidgetToImage(state->forgeRadial); + + auto giftsTotal = 0; + auto lostIndex = 0; + for (auto i = 0; i != 4; ++i) { + auto &entry = state->entries[i]; + auto &corner = craftState->corners[i]; + + if (entry.button) { + corner.originalRect = entry.button->geometry(); + if (entry.percent) { + corner.percentBadge = GrabWidgetToImage(entry.percent); + } + if (entry.remove) { + corner.removeButton = GrabWidgetToImage(entry.remove); + } + if (lostIndex < craftState->lostGifts.size()) { + craftState->lostGifts[lostIndex].cornerIndex = i; + ++lostIndex; + } + corner.giftButton.reset(entry.button); + entry.button->setParent(parent); + base::take(entry.button)->hide(); + + ++giftsTotal; + } else if (entry.add) { + corner.addButton = GrabWidgetToImage(entry.add); + corner.originalRect = entry.add->geometry(); + } + } + + const auto failedCount = MakeRadialPercent( + raw, + st::craftForgePercent, + rpl::single(0), + [=](int) { return QString::number(giftsTotal); }); + failedCount->setGeometry(state->forgeRadial->geometry()); + craftState->lostRadial = GrabWidgetToImage(failedCount); + delete failedCount; + + const auto overlayBg = craftState->forgeBgOverlay = ForgeBgOverlay(); + const auto backdrop = CraftBackdrops()[giftsTotal - 1].backdrop; + craftState->forgeBg1 = anim::color( + backdrop.centerColor, + QColor(overlayBg.red(), overlayBg.green(), overlayBg.blue()), + overlayBg.alphaF()); + craftState->forgeBg2 = anim::color( + backdrop.edgeColor, + QColor(overlayBg.red(), overlayBg.green(), overlayBg.blue()), + overlayBg.alphaF()); + const auto overlayFail = ForgeFailOverlay(); + craftState->forgeFail = anim::color( + CraftBackdrops().back().backdrop.centerColor, + QColor(overlayFail.red(), overlayFail.green(), overlayFail.blue()), + overlayFail.alphaF()); + for (auto i = 0; i != 6; ++i) { + craftState->forgeSides[i] = craftState->prepareEmptySide(i); + } + }; + + return { + .widget = std::move(widget), + .editRequests = state->editRequests.events(), + .removeRequests = state->removeRequests.events(), + .grabForAnimation = std::move(grabForAnimation), + }; +} + +void AddCraftGiftsList( + not_null window, + not_null container, + Data::CraftGiftsDescriptor descriptor, + const std::vector &selected, + Fn)> chosen) { + struct State { + rpl::event_stream<> updated; + Data::CraftGiftsDescriptor data; + rpl::variable empty = true; + rpl::lifetime loading; + }; + const auto state = container->lifetime().make_state(); + state->data = std::move(descriptor); + + using Descriptor = Info::PeerGifts::GiftDescriptor; + using StarGift = Info::PeerGifts::GiftTypeStars; + auto handler = crl::guard(container, [=](Descriptor descriptor) { + Expects(v::is(descriptor)); + + const auto unique = v::get(descriptor).info.unique; + chosen(unique); + }); + + auto gifts = rpl::single( + rpl::empty + ) | rpl::then(state->updated.events()) | rpl::map([=] { + auto result = GiftsDescriptor(); + const auto selfId = window->session().userPeerId(); + for (const auto &gift : state->data.list) { + result.list.push_back(Info::PeerGifts::GiftTypeStars{ + .info = gift.info, + .resale = true, + .mine = (gift.info.unique->ownerId == selfId), + }); + } + state->empty = result.list.empty(); + return result; + }); + const auto peer = window->session().user(); + const auto loadMore = [=] { + if (!state->data.offset.isEmpty() && !state->loading) { + state->loading = Data::CraftGiftsSlice( + &peer->session(), + state->data.giftId, + state->data.offset + ) | rpl::on_next([=](Data::CraftGiftsDescriptor &&slice) { + state->loading.destroy(); + state->data.offset = slice.list.empty() + ? QString() + : slice.offset; + state->data.list.insert( + end(state->data.list), + std::make_move_iterator(begin(slice.list)), + std::make_move_iterator(end(slice.list))); + state->updated.fire({}); + }); + } + }; + container->add(MakeGiftsList({ + .window = window, + .mode = GiftsListMode::Craft, + .peer = peer, + .gifts = std::move(gifts), + .selected = (selected + | ranges::views::transform(&GiftForCraft::unique) + | ranges::to_vector), + .loadMore = loadMore, + .handler = handler, + })); + + const auto skip = st::defaultSubsectionTitlePadding.top(); + const auto wrap = container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_gift_craft_select_none(), + st::craftYourListEmpty), + (st::boxRowPadding + QMargins(0, 0, 0, skip))), + style::al_top); + state->empty.value() | rpl::on_next([=](bool empty) { + // Scroll doesn't jump up if we show before rows are cleared, + // and we hide after rows are added. + if (empty) { + wrap->show(anim::type::instant); + } else { + crl::on_main(wrap, [=] { + if (!state->empty.current()) { + wrap->hide(anim::type::instant); + } + }); + } + }, wrap->lifetime()); + wrap->entity()->setTryMakeSimilarLines(true); +} + +void ShowSelectGiftBox( + not_null controller, + uint64 giftId, + Fn chosen, + std::vector selected, + Fn boxClosed) { + struct Entry { + Data::SavedStarGift gift; + GiftButton *button = nullptr; + }; + struct State { + std::vector entries; + + Data::CraftGiftsDescriptor craft; + Data::ResaleGiftsDescriptor resale; + + rpl::lifetime craftLifetime; + rpl::lifetime resaleLifetime; + }; + + const auto session = &controller->session(); + const auto state = std::make_shared(); + + const auto make = [=](not_null box) { + box->setTitle(tr::lng_gift_craft_select_title()); + box->setWidth(st::boxWideWidth); + + box->boxClosing() | rpl::on_next(boxClosed, box->lifetime()); + + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_gift_craft_select_your()); + + const auto got = crl::guard(box, [=]( + std::shared_ptr gift) { + if (!ShowCraftLaterError(box->uiShow(), gift)) { + chosen(GiftForCraft{ .unique = gift }); + box->closeBox(); + } + }); + + AddCraftGiftsList( + controller, + box->verticalLayout(), + state->craft, + selected, + got); + + if (const auto count = state->resale.count) { + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_gift_craft_select_market( + lt_count, + rpl::single(count * 1.))); + + AddResaleGiftsList( + controller, + session->user(), + box->verticalLayout(), + state->resale, + rpl::single(false), + got, + true); + } + + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + box->setMaxHeight(st::boxWideWidth); + }; + const auto show = crl::guard(controller, [=] { + controller->show(Box(make)); + }); + + state->craftLifetime = Data::CraftGiftsSlice( + session, + giftId + ) | rpl::on_next([=](Data::CraftGiftsDescriptor &&info) { + state->craftLifetime.destroy(); + + state->craft = std::move(info); + if (state->resale.giftId) { + show(); + } + }); + + state->resaleLifetime = Data::ResaleGiftsSlice( + session, + giftId, + { .forCraft = true } + ) | rpl::on_next([=](Data::ResaleGiftsDescriptor &&info) { + state->resaleLifetime.destroy(); + + state->resale = std::move(info); + if (state->craft.giftId) { + show(); + } + }); +} + +[[nodiscard]] object_ptr MakeRarityExpectancyPreview( + not_null parent, + not_null session, + rpl::producer> gifts) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + struct AttributeState { + rpl::variable permille; + bool isBackdrop = false; + QString name; + + Animations::Simple backdropAnimation; + Data::UniqueGiftBackdrop wasBackdrop; + Data::UniqueGiftBackdrop nowBackdrop; + QImage wasFrame; + QImage nowFrame; + + Animations::Simple patternAnimation; + DocumentData *wasPattern = nullptr; + DocumentData *nowPattern = nullptr; + std::unique_ptr wasEmoji; + std::unique_ptr nowEmoji; + + AbstractButton *button = nullptr; + RpWidget *radial = nullptr; + }; + + struct State { + std::array attrs; + int count = 0; + ImportantTooltip *tooltip = nullptr; + }; + + const auto state = raw->lifetime().make_state(); + + const auto single = st::craftAttributeSize; + const auto skip = st::craftAttributeSkip; + + for (auto i = 0; i != 8; ++i) { + auto &attr = state->attrs[i]; + const auto btn = CreateChild(raw); + attr.button = btn; + btn->resize(single, single); + btn->hide(); + + const auto idx = i; + btn->paintOn([=](QPainter &p) { + auto &a = state->attrs[idx]; + const auto sub = st::craftAttributePadding; + const auto inner = QRect(0, 0, single, single).marginsRemoved( + { sub, sub, sub, sub }); + if (a.isBackdrop) { + const auto progress = a.backdropAnimation.value(1.); + if (progress < 1.) { + p.drawImage(inner.topLeft(), a.wasFrame); + p.setOpacity(progress); + } + if (a.nowFrame.isNull()) { + const auto ratio = style::DevicePixelRatio(); + a.nowFrame = QImage( + inner.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + a.nowFrame.fill(Qt::transparent); + a.nowFrame.setDevicePixelRatio(ratio); + auto q = QPainter(&a.nowFrame); + auto hq = PainterHighQualityEnabler(q); + auto gradient = QLinearGradient( + QPointF(inner.width(), 0), + QPointF(0, inner.height())); + gradient.setColorAt(0., a.nowBackdrop.centerColor); + gradient.setColorAt(1., a.nowBackdrop.edgeColor); + q.setPen(Qt::NoPen); + q.setBrush(gradient); + q.drawEllipse( + 0, + 0, + inner.width(), + inner.height()); + + q.setCompositionMode( + QPainter::CompositionMode_Source); + q.setBrush(Qt::transparent); + const auto max = u"100%"_q; + const auto &font = st::craftAttributePercent.font; + const auto tw = font->width(max); + q.drawRoundedRect( + (inner.width() - tw) / 2, + inner.height() + sub - font->height, + tw, + font->height, + font->height / 2., + font->height / 2.); + } + p.drawImage(inner.topLeft(), a.nowFrame); + p.setOpacity(1.); + } else { + const auto center = QRect(0, 0, single, single).center(); + const auto emojiShift + = (single - Emoji::GetCustomSizeNormal()) / 2; + const auto pos = QPoint(emojiShift, emojiShift); + const auto progress = a.patternAnimation.value(1.); + if (progress < 1.) { + p.translate(center); + p.save(); + p.setOpacity(1. - progress); + p.scale(1. - progress, 1. - progress); + p.translate(-center); + a.wasEmoji->paint(p, { + .textColor = st::white->c, + .position = pos, + }); + p.restore(); + p.scale(progress, progress); + p.setOpacity(progress); + p.translate(-center); + } + if (!a.nowEmoji) { + a.nowEmoji = session->data().customEmojiManager().create( + a.nowPattern, + [=] { btn->update(); }); + } + a.nowEmoji->paint(p, { + .textColor = st::white->c, + .position = pos, + }); + } + }); + + attr.radial = MakeRadialPercent( + btn, + st::craftAttributePercent, + attr.permille.value()); + attr.radial->setGeometry(0, 0, single, single); + + btn->setClickedCallback([=] { + auto &a = state->attrs[idx]; + if (state->tooltip) { + state->tooltip->toggleAnimated(false); + } + + auto text = a.isBackdrop + ? tr::lng_gift_craft_chance_backdrop( + tr::now, + lt_percent, + TextWithEntities{ FormatPercent(a.permille.current()) }, + lt_name, + TextWithEntities{ a.name }, + Ui::Text::RichLangValue) + : tr::lng_gift_craft_chance_symbol( + tr::now, + lt_percent, + TextWithEntities{ FormatPercent(a.permille.current()) }, + lt_name, + TextWithEntities{ a.name }, + Ui::Text::RichLangValue); + const auto tooltip = CreateChild( + parent, + MakeNiceTooltipLabel( + parent, + rpl::single(std::move(text)), + st::boxWideWidth / 2, + st::defaultImportantTooltipLabel, + st::defaultPopupMenu), + st::defaultImportantTooltip); + tooltip->toggleFast(false); + + base::install_event_filter(tooltip, qApp, [=](not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + tooltip->toggleAnimated(false); + } + return base::EventFilterResult::Continue; + }); + + const auto geometry = MapFrom(parent, btn, btn->rect()); + const auto countPosition = [=](QSize size) { + const auto left = geometry.x() + + (geometry.width() - size.width()) / 2; + const auto right = parent->width() + - st::normalFont->spacew; + return QPoint( + std::max(std::min(left, right - size.width()), 0), + geometry.y() + - size.height() + - st::normalFont->descent); + }; + tooltip->pointAt(geometry, RectPart::Top, countPosition); + tooltip->toggleAnimated(true); + + state->tooltip = tooltip; + tooltip->shownValue() | rpl::filter( + !rpl::mappers::_1 + ) | rpl::on_next([=] { + crl::on_main(tooltip, [=] { + if (tooltip->isHidden()) { + if (state->tooltip == tooltip) { + state->tooltip = nullptr; + } + delete tooltip; + } + }); + }, tooltip->lifetime()); + + base::timer_once( + 3000 + ) | rpl::on_next([=] { + tooltip->toggleAnimated(false); + }, tooltip->lifetime()); + }); + } + + const auto relayout = [=](int count) { + const auto twoRows = (count > 5); + const auto row1 = twoRows ? ((count + 1) / 2) : count; + const auto row2 = twoRows ? (count - row1) : 0; + const auto rowWidth = [&](int n) { + return n * single + (n - 1) * skip; + }; + const auto w1 = rowWidth(row1); + const auto w2 = row2 ? rowWidth(row2) : 0; + const auto naturalWidth = std::max(w1, w2); + const auto totalHeight = twoRows + ? (2 * single + st::craftAttributeRowSkip) + : single; + + for (auto i = 0; i != 8; ++i) { + auto &attr = state->attrs[i]; + if (i < count) { + const auto inRow1 = (i < row1); + const auto rowItems = inRow1 ? row1 : row2; + const auto rowW = rowWidth(rowItems); + const auto indexInRow = inRow1 ? i : (i - row1); + const auto x = (naturalWidth - rowW) / 2 + + indexInRow * (single + skip); + const auto y = inRow1 + ? 0 + : (single + st::craftAttributeRowSkip); + attr.button->setGeometry(x, y, single, single); + attr.button->show(); + } else { + attr.button->hide(); + } + } + + raw->setNaturalWidth(naturalWidth); + raw->resize(naturalWidth, totalHeight); + }; + + const auto permilles = session->appConfig().craftAttributePermilles(); + const auto computePermille = [=](int total, int count) { + Expects(total > 0); + Expects(count > 0); + + const auto &list = (total <= permilles.size()) + ? permilles[total - 1] + : std::vector(); + return (count <= list.size()) ? list[count - 1] : 1000; + }; + + std::move( + gifts + ) | rpl::on_next([=](const std::vector &list) { + struct BackdropEntry { + Data::UniqueGiftBackdrop fields; + QString name; + int count = 0; + }; + struct PatternEntry { + not_null document; + QString name; + int count = 0; + }; + auto backdrops = std::vector(); + auto patterns = std::vector(); + for (const auto &gift : list) { + const auto &backdrop = gift.unique->backdrop; + const auto &pattern = gift.unique->pattern; + const auto proj1 = &BackdropEntry::fields; + const auto i = ranges::find(backdrops, backdrop, proj1); + if (i != end(backdrops)) { + ++i->count; + } else { + backdrops.push_back({ backdrop, backdrop.name, 1 }); + } + const auto proj2 = &PatternEntry::document; + const auto j = ranges::find( + patterns, + pattern.document, + proj2); + if (j != end(patterns)) { + ++j->count; + } else { + patterns.push_back({ pattern.document, pattern.name, 1 }); + } + } + ranges::sort(backdrops, ranges::greater(), &BackdropEntry::count); + ranges::sort(patterns, ranges::greater(), &PatternEntry::count); + + const auto total = int(list.size()); + auto slotIndex = 0; + for (const auto &b : backdrops) { + if (slotIndex >= 8) break; + auto &a = state->attrs[slotIndex]; + a.isBackdrop = true; + a.name = b.name; + a.permille = computePermille(total, b.count); + if (a.nowBackdrop != b.fields) { + if (!a.nowFrame.isNull()) { + a.wasBackdrop = a.nowBackdrop; + a.wasFrame = base::take(a.nowFrame); + a.backdropAnimation.stop(); + a.backdropAnimation.start([=, btn = a.button] { + btn->update(); + }, 0., 1., st::fadeWrapDuration); + } + a.nowBackdrop = b.fields; + a.nowFrame = QImage(); + } + ++slotIndex; + } + for (const auto &pt : patterns) { + if (slotIndex >= 8) break; + auto &a = state->attrs[slotIndex]; + a.isBackdrop = false; + a.name = pt.name; + a.permille = computePermille(total, pt.count); + if (a.nowPattern != pt.document) { + if (a.nowEmoji) { + a.wasPattern = a.nowPattern; + a.wasEmoji = base::take(a.nowEmoji); + a.patternAnimation.stop(); + a.patternAnimation.start([=, btn = a.button] { + btn->update(); + }, 0., 1., st::fadeWrapDuration); + } + a.nowPattern = pt.document; + a.nowEmoji = nullptr; + } + ++slotIndex; + } + + const auto newCount = slotIndex; + if (state->count != newCount) { + state->count = newCount; + relayout(newCount); + } + }, raw->lifetime()); + + return result; +} + +void Craft( + not_null box, + not_null controller, + std::shared_ptr state, + const std::vector &gifts) { + auto show = controller->uiShow(); + auto startRequest = [=](Fn done) { +#if 0 + constexpr auto kDelays = std::array{ + 100, 200, 300, 400, 500, 1000, 2000 + }; + const auto delay = kDelays[base::RandomIndex(kDelays.size())]; + const auto giftsCopy = gifts; + + base::call_delayed(delay, box, [=] { + //static auto testing = 0; + const auto shouldSucceed = false;// (((++testing) / 4) % 2 == 0); + const auto count = int(giftsCopy.size()); + if (shouldSucceed && count > 0) { + const auto &chosen = giftsCopy[base::RandomIndex(count)]; + auto info = Data::StarGift{ + .id = chosen.unique->initialGiftId, + .unique = chosen.unique, + .document = chosen.unique->model.document, + }; + auto result = std::make_shared( + Data::GiftUpgradeResult{ + .info = std::move(info), + .manageId = chosen.manageId, + }); + done(std::move(result)); + } else { + done(nullptr); + } + }); +#endif + + auto inputs = QVector(); + for (const auto &gift : gifts) { + inputs.push_back( + Api::InputSavedStarGiftId(gift.manageId, gift.unique)); + } + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + session->api().request(MTPpayments_CraftStarGift( + MTP_vector(inputs) + )).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + session->data().nextForUpgradeGiftInvalidate(session->user()); + const auto gift = FindUniqueGift(session, result); + const auto slug = gift ? gift->info.unique->slug : QString(); + for (const auto &input : gifts) { + const auto action = (slug == input.unique->slug) + ? Data::GiftUpdate::Action::Upgraded + : Data::GiftUpdate::Action::Delete; + controller->session().data().notifyGiftUpdate({ + .id = input.manageId, + .slug = input.unique->slug, + .action = action, + }); + } + done(gift); + }).fail([=](const MTP::Error &error) { + const auto type = error.type(); + const auto waitPrefix = u"STARGIFT_CRAFT_TOO_EARLY_"_q; + if (type.startsWith(waitPrefix)) { + done(CraftResultWait{ + .seconds = type.mid(waitPrefix.size()).toInt(), + }); + } else { + done(CraftResultError{ type }); + } + }).send(); + }; + const auto requested = std::make_shared(); + const auto giftId = gifts.front().unique->initialGiftId; + auto retryWithNewGift = [=](Fn closeCurrent) { + if (*requested) { + return; + } + *requested = true; + ShowSelectGiftBox(controller, giftId, [=](GiftForCraft chosen) { + ShowGiftCraftBox(controller, { chosen }, false); + closeCurrent(); + }, {}, [=] { *requested = false; }); + }; + StartCraftAnimation( + box, + std::move(show), + std::move(state), + std::move(startRequest), + std::move(retryWithNewGift)); +} + +void AddPreviewNewModels( + not_null container, + std::shared_ptr show, + const QString &giftName, + Data::UniqueGiftAttributes attributes, + rpl::producer visible) { + auto exceptional = std::vector(); + for (const auto &model : attributes.models) { + if (Data::UniqueGiftAttributeHasSpecialRarity(model)) { + exceptional.push_back(model); + } + } + attributes.models = std::move(exceptional); + + auto emoji = TextWithEntities(); + const auto indices = RandomIndicesSubset( + int(attributes.models.size()), + std::min(3, int(attributes.models.size()))); + for (const auto index : indices) { + emoji.append(Data::SingleCustomEmoji( + attributes.models[index].document)); + } + + auto badge = object_ptr(container); + const auto badgeRaw = badge.data(); + + const auto label = CreateChild( + badgeRaw, + tr::lng_gift_craft_view_all( + lt_emoji, + rpl::single(emoji), + lt_arrow, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Text::WithEntities), + st::uniqueGiftResalePrice, + st::defaultPopupMenu, + Core::TextContext({ .session = &show->session() })); + label->setTextColorOverride(st::white->c); + + label->sizeValue() | rpl::on_next([=](QSize size) { + const auto padding = st::craftPreviewAllPadding; + badgeRaw->setNaturalWidth( + padding.left() + size.width() + padding.right()); + badgeRaw->resize( + badgeRaw->naturalWidth(), + padding.top() + size.height() + padding.bottom()); + }, label->lifetime()); + + badgeRaw->widthValue() | rpl::on_next([=](int width) { + const auto padding = st::craftPreviewAllPadding; + label->move(padding.left(), padding.top()); + }, badgeRaw->lifetime()); + + badgeRaw->paintOn([=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + const auto rect = badgeRaw->rect(); + const auto radius = rect.height() / 2.; + p.setPen(Qt::NoPen); + p.setBrush(ForgeBgOverlay()); + p.drawRoundedRect(rect, radius, radius); + }); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + badgeRaw->setClickedCallback([=] { + auto previewAttrs = attributes; + previewAttrs.models.erase( + ranges::remove_if(previewAttrs.models, [](const auto &m) { + return !Data::UniqueGiftAttributeHasSpecialRarity(m); + }), + end(previewAttrs.models)); + show->show(Box( + StarGiftPreviewBox, + giftName, + previewAttrs, + Data::GiftAttributeIdType::Model, + nullptr)); + }); + + const auto wrap = container->add( + object_ptr>( + container, + std::move(badge), + st::craftPreviewAllMargin), + style::al_top); + std::move(visible) | rpl::on_next([=](bool shown) { + wrap->toggle(shown, anim::type::instant); + }, wrap->lifetime()); +} + +void MakeCraftContent( + not_null box, + not_null controller, + std::vector gifts, + bool autoStartCraft) { + Expects(!gifts.empty()); + + struct State { + std::shared_ptr craftState; + GradientButton *button = nullptr; + + FlatLabel *title = nullptr; + FlatLabel *about = nullptr; + RpWidget *attributes = nullptr; + RpWidget *craftingView = nullptr; + Fn)> grabCraftingView; + + rpl::variable> chosen; + rpl::variable name; + rpl::variable successPercentPermille; + rpl::variable successPercentText; + + int requestingIndex = -1; + bool crafting = false; + }; + const auto session = &controller->session(); + const auto giftId = gifts.front().unique->initialGiftId; + const auto state = box->lifetime().make_state(); + + const auto auctions = &controller->session().giftAuctions(); + auto attributes = auctions->attributes(giftId).value_or( + Data::UniqueGiftAttributes()); + + state->craftState = std::make_shared(); + state->craftState->session = session; + state->craftState->coversAnimate = true; + + { + auto backdrops = CraftBackdrops(); + const auto emoji = Text::IconEmoji(&st::craftPattern); + const auto data = emoji.entities.front().data(); + for (auto i = 0; i != backdrops.size(); ++i) { + auto &cover = state->craftState->covers[i]; + cover.backdrop.colors = backdrops[i].backdrop; + cover.pattern.emoji = Text::TryMakeSimpleEmoji(data); + cover.button1 = backdrops[i].button1; + cover.button2 = backdrops[i].button2; + } + } + + const auto giftName = gifts.front().unique->title; + state->chosen = std::move(gifts); + for (auto i = 0; i != int(state->chosen.current().size()); ++i) { + state->craftState->covers[i].shown = true; + } + + state->name = state->chosen.value( + ) | rpl::map([=](const std::vector &gifts) { + return Data::UniqueGiftName(*gifts.front().unique); + }); + state->successPercentPermille = state->chosen.value( + ) | rpl::map([=](const std::vector &gifts) { + auto result = 0; + for (const auto &entry : gifts) { + if (const auto gift = entry.unique.get()) { + result += gift->craftChancePermille; + } + } + return result; + }); + state->successPercentText = state->successPercentPermille.value( + ) | rpl::map(FormatPercent); + + const auto raw = box->verticalLayout(); + + state->chosen.value( + ) | rpl::on_next([=](const std::vector &gifts) { + state->craftState->updateForGiftCount(int(gifts.size()), [=] { + raw->update(); + }); + }, box->lifetime()); + + const auto title = state->title = raw->add( + object_ptr( + box, + tr::lng_gift_craft_title(), + st::uniqueGiftTitle), + st::craftTitleMargin, + style::al_top); + title->setTextColorOverride(QColor(255, 255, 255)); + + auto crafting = MakeCraftingView( + raw, + session, + state->chosen.value(), + state->craftState->edgeColor.value()); + const auto craftingHeight = crafting.widget->height(); + state->craftingView = crafting.widget.data(); + state->grabCraftingView = std::move(crafting.grabForAnimation); + raw->add(std::move(crafting.widget)); + std::move(crafting.removeRequests) | rpl::on_next([=](int index) { + auto chosen = state->chosen.current(); + if (index < chosen.size()) { + chosen.erase(begin(chosen) + index); + state->chosen = std::move(chosen); + } + }, raw->lifetime()); + + std::move( + crafting.editRequests + ) | rpl::on_next([=](int index) { + const auto guard = base::make_weak(raw); + if (state->requestingIndex >= 0) { + state->requestingIndex = index; + return; + } + state->requestingIndex = index; + const auto callback = [=](GiftForCraft chosen) { + auto copy = state->chosen.current(); + if (state->requestingIndex < copy.size()) { + copy[state->requestingIndex] = chosen; + } else { + copy.push_back(chosen); + } + state->chosen = std::move(copy); + }; + ShowSelectGiftBox( + controller, + giftId, + crl::guard(raw, callback), + state->chosen.current(), + crl::guard(raw, [=] { state->requestingIndex = -1; })); + }, raw->lifetime()); + + auto fullName = state->chosen.value( + ) | rpl::map([=](const std::vector &list) { + const auto unique = list.front().unique.get(); + return Data::SingleCustomEmoji( + unique->model.document + ).append(' ').append(tr::bold(Data::UniqueGiftName(*unique))); + }); + auto aboutText = rpl::combine( + tr::lng_gift_craft_about1(lt_gift, fullName, tr::rich), + tr::lng_gift_craft_about2(tr::rich) + ) | rpl::map([=](TextWithEntities &&a, TextWithEntities &&b) { + return a.append('\n').append('\n').append(b); + }); + const auto about = state->about = raw->add( + object_ptr( + raw, + std::move(aboutText), + st::craftAbout, + st::defaultPopupMenu, + Core::TextContext({ .session = session })), + st::craftAboutMargin, + style::al_top); + about->setTextColorOverride(st::white->c); + about->setTryMakeSimilarLines(true); + + state->attributes = raw->add( + MakeRarityExpectancyPreview(raw, session, state->chosen.value()), + st::craftAttributesMargin, + style::al_top); + + auto viewAllVisible = state->chosen.value( + ) | rpl::map([](const std::vector &list) { + auto backdropIds = base::flat_set(); + auto patternPtrs = base::flat_set(); + for (const auto &gift : list) { + backdropIds.emplace(gift.unique->backdrop.id); + patternPtrs.emplace(gift.unique->pattern.document.get()); + } + return int(backdropIds.size() + patternPtrs.size()) <= 5; + }); + + AddPreviewNewModels( + raw, + controller->uiShow(), + giftName, + std::move(attributes), + std::move(viewAllVisible)); + + raw->paintOn([=](QPainter &p) { + const auto &cs = state->craftState; + if (cs->craftingStarted) { + return; + } + const auto wasButton1 = cs->button1; + const auto wasButton2 = cs->button2; + cs->paint(p, raw->size(), craftingHeight); + if (cs->button1 != wasButton1 || cs->button2 != wasButton2) { + state->button->setStops({ + { 0., cs->button1 }, + { 1., cs->button2 }, + }); + } + }); + + const auto button = state->button = raw->add( + object_ptr(raw, QGradientStops()), + st::craftBoxButtonPadding); + button->setFullRadius(true); + button->startGlareAnimation(); + + const auto startCrafting = [=] { + if (state->crafting) { + 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(); + + for (const auto &gift : gifts) { + if (gift.unique) { + cs->lostGifts.push_back({ + .number = u"#"_q + Lang::FormatCountDecimal(gift.unique->number), + }); + } + } + + if (state->grabCraftingView) { + state->grabCraftingView(cs); + } + + const auto craftingPos = state->craftingView->pos(); + cs->craftingTop = craftingPos.y(); + cs->craftingBottom = craftingPos.y() + state->craftingView->height(); + + for (auto &corner : cs->corners) { + corner.originalRect.translate(craftingPos); + } + cs->forgeRect.translate(craftingPos); + cs->craftingAreaCenterY = cs->forgeRect.center().y(); + + const auto aboutPos = state->about->pos(); + const auto buttonY = state->button->pos().y(); + const auto buttonBottom = buttonY + state->button->height(); + cs->originalButtonY = buttonY; + const auto bottomRect = QRect( + 0, + aboutPos.y(), + raw->width(), + buttonBottom - aboutPos.y()); + + const auto ratio = style::DevicePixelRatio(); + auto bottomPart = QImage( + bottomRect.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + bottomPart.fill(Qt::transparent); + bottomPart.setDevicePixelRatio(ratio); + + const auto renderWidget = [&](QWidget *widget) { + const auto pos = widget->pos() - bottomRect.topLeft(); + widget->render(&bottomPart, pos, {}, QWidget::DrawChildren); + }; + renderWidget(state->about); + renderWidget(state->attributes); + renderWidget(state->button); + + cs->bottomPart = std::move(bottomPart); + cs->bottomPartY = aboutPos.y(); + cs->containerHeight = raw->height(); + + Craft( + box, + controller, + state->craftState, + state->chosen.current()); + }; + button->setClickedCallback(startCrafting); + + SetButtonTwoLabels( + button, + st::giftBox.button.textTop, + tr::lng_gift_craft_button( + lt_gift, + state->name.value() | rpl::map(tr::marked), + tr::marked), + tr::lng_gift_craft_button_chance( + lt_percent, + state->successPercentText.value() | rpl::map(tr::marked), + tr::marked), + st::resaleButtonTitle, + st::resaleButtonSubtitle); + + raw->widthValue() | rpl::on_next([=](int width) { + const auto padding = st::craftBoxButtonPadding; + button->setNaturalWidth(width - padding.left() - padding.right()); + button->resize(button->naturalWidth(), st::giftBox.button.height); + }, raw->lifetime()); + + if (autoStartCraft) { +// base::call_delayed(crl::time(1000), raw, startCrafting); + } +} + +void ShowGiftCraftBox( + not_null controller, + std::vector gifts, + bool autoStartCraft) { + controller->show(Box([=](not_null box) { + box->setStyle(st::giftCraftBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + MakeCraftContent( + box, + controller, + gifts, + autoStartCraft); + AddUniqueCloseButton(box); + +#if _DEBUG + if (autoStartCraft) { + static const auto full = gifts; + box->boxClosing() | rpl::on_next([=] { + base::call_delayed(1000, controller, [=] { + auto copy = gifts; + if (copy.size() > 1) { + copy.pop_back(); + } else { + copy = full; + } + ShowGiftCraftBox(controller, copy, true); + }); + }, box->lifetime()); + } +#endif + })); +} + +} // namespace + +void ShowGiftCraftInfoBox( + not_null controller, + std::shared_ptr gift, + Data::SavedStarGiftId savedId) { + controller->show(Box([=](not_null box) { + const auto container = box->verticalLayout(); + auto cover = tr::lng_gift_craft_info_title( + ) | rpl::map([=](const QString &title) { + auto result = UniqueGiftCover{ *gift }; + result.values.title = title; + return result; + }); + AddUniqueGiftCover(container, std::move(cover), { + .subtitle = tr::lng_gift_craft_info_about(tr::marked), + }); + AddSkip(container); + + AddUniqueCloseButton(box); + + const auto features = std::vector{ + { + st::menuIconUnique, + tr::lng_gift_craft_rare_title(tr::now), + tr::lng_gift_craft_rare_about(tr::now, tr::rich), + }, + { + st::menuIconCraftTraits, + tr::lng_gift_craft_chance_title(tr::now), + tr::lng_gift_craft_chance_about(tr::now, tr::marked), + }, + { + st::menuIconCraftChance, + tr::lng_gift_craft_affect_title(tr::now), + tr::lng_gift_craft_affect_about(tr::now, tr::marked), + }, + }; + for (const auto &feature : features) { + container->add( + MakeFeatureListEntry(container, feature), + st::boxRowPadding); + } + + box->setStyle(st::giftBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + + box->addButton(tr::lng_gift_craft_start_button(), [=] { + const auto auctions = &controller->session().giftAuctions(); + const auto show = crl::guard(box, [=] { + ShowGiftCraftBox( + controller, + { GiftForCraft{ gift, savedId } }, + false); + box->closeBox(); + }); + if (auctions->attributes(gift->initialGiftId)) { + show(); + } else { + auctions->requestAttributes(gift->initialGiftId, show); + } + }); + })); +} + +void ShowTestGiftCraftBox( + not_null controller, + std::vector gifts) { + auto converted = std::vector(); + converted.reserve(gifts.size()); + for (auto &gift : gifts) { + auto entry = GiftForCraft(); + entry.unique = std::move(gift.unique); + entry.manageId = std::move(gift.manageId); + converted.push_back(std::move(entry)); + } + ShowGiftCraftBox(controller, std::move(converted), true); +} + +bool ShowCraftLaterError( + std::shared_ptr show, + std::shared_ptr gift) { + const auto now = base::unixtime::now(); + if (gift->canCraftAt <= now) { + return false; + } + ShowCraftLaterError(show, gift->canCraftAt); + return true; +} + +void ShowCraftLaterError( + std::shared_ptr show, + TimeId when) { + const auto data = base::unixtime::parse(when); + const auto time = QLocale().toString(data.time(), QLocale::ShortFormat); + const auto date = langDayOfMonthShort(data.date()); + + show->show(MakeInformBox({ + .text = tr::lng_gift_craft_when( + lt_date, + rpl::single(tr::bold(date)), + lt_time, + rpl::single(tr::bold(time)), + tr::marked), + .title = tr::lng_gift_craft_unavailable(), + })); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.h b/Telegram/SourceFiles/boxes/star_gift_craft_box.h new file mode 100644 index 0000000000..283235668e --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.h @@ -0,0 +1,46 @@ +/* +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 "data/data_star_gift.h" + +namespace Data { +struct UniqueGift; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { + +class Show; + +struct GiftForCraftEntry { + std::shared_ptr unique; + Data::SavedStarGiftId manageId; +}; + +void ShowGiftCraftInfoBox( + not_null controller, + std::shared_ptr gift, + Data::SavedStarGiftId savedId); + +void ShowTestGiftCraftBox( + not_null controller, + std::vector gifts); + +[[nodiscard]] bool ShowCraftLaterError( + std::shared_ptr show, + std::shared_ptr gift); + +void ShowCraftLaterError( + std::shared_ptr show, + TimeId when); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp b/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp index 879bc91270..1c35a778e9 100644 --- a/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/top_background_gradient.h" #include "settings/settings_credits_graphics.h" +#include "styles/style_chat.h" #include "styles/style_credits.h" #include "styles/style_layers.h" @@ -200,10 +201,13 @@ public: QWidget *parent, not_null delegate, not_null attributes, + std::vector otherModels, rpl::producer tab, Selection initialSelection); [[nodiscard]] rpl::producer selected() const; + [[nodiscard]] auto modelsToggled() const + -> rpl::producer>; private: struct Entry { @@ -231,6 +235,7 @@ private: int resizeGetHeight(int width) override; void clicked(int index); + void toggleModels(); const not_null _delegate; const not_null _attributes; @@ -256,8 +261,23 @@ private: int _visibleFrom = 0; int _visibleTill = 0; + bool _craftedModels = false; + std::vector _activeModels; + std::vector _otherModels; + std::unique_ptr _toggleLink; + rpl::event_stream> _modelsToggled; + }; +[[nodiscard]] QColor MakeOpaque(QColor color, QColor bg) { + return (color.alpha() == 255) + ? color + : anim::color( + bg, + QColor(color.red(), color.green(), color.blue(), 255), + color.alphaF()); +} + void CacheBackdropBackground( not_null backdrop, int width, @@ -309,7 +329,7 @@ void AttributeButton::setup() { _name.setText(st::uniqueAttributeName, data.name); _percent.setText( st::uniqueAttributePercent, - QString::number(data.rarityPermille / 10.) + '%'); + Data::UniqueGiftAttributeText(data)); }); v::match(_descriptor, [&](const GiftBackdrop &data) { @@ -470,7 +490,12 @@ void AttributeButton::paintBackground( return; } const auto pwidth = progress * st::defaultRoundCheckbox.width; - p.setPen(QPen(st::defaultRoundCheckbox.bgActive->c, pwidth)); + const auto model = std::get_if(&_descriptor); + const auto color = (!model + || (model->rarityType() == Data::UniqueGiftRarity::Default)) + ? st::defaultRoundCheckbox.bgActive->c + : Data::UniqueGiftRarityBadgeColors(model->rarityType()).fg; + p.setPen(QPen(color, pwidth)); p.setBrush(Qt::NoBrush); const auto rounded = rect().marginsRemoved(_extend); const auto phalf = pwidth / 2.; @@ -650,11 +675,15 @@ void AttributeButton::paintEvent(QPaintEvent *e) { }); p.setPen(Qt::NoPen); - p.setBrush(model - ? anim::color(st::windowBgOver, st::windowBgActive, progress) - : backdrop + p.setBrush(backdrop ? backdrop->patternColor - : _delegate->patternColor()); + : !model + ? _delegate->patternColor() + : (model->rarityType() == Data::UniqueGiftRarity::Default) + ? anim::color(st::windowBgOver, st::windowBgActive, progress) + : MakeOpaque( + Data::UniqueGiftRarityBadgeColors(model->rarityType()).bg, + st::boxBg->c)); const auto ppadding = st::uniqueAttributePercentPadding; const auto add = int(std::ceil(style::ConvertScaleExact(6.))); const auto pradius = std::max(st::giftBoxGiftRadius - add, 1); @@ -670,9 +699,11 @@ void AttributeButton::paintEvent(QPaintEvent *e) { _percent.maxWidth(), st::uniqueAttributeType.font->height); p.drawRoundedRect(percent.marginsAdded(ppadding), pradius, pradius); - p.setPen(model + p.setPen(!model + ? QColor(255, 255, 255) + : (model->rarityType() == Data::UniqueGiftRarity::Default) ? anim::color(st::windowSubTextFg, st::windowFgActive, progress) - : QColor(255, 255, 255)); + : Data::UniqueGiftRarityBadgeColors(model->rarityType()).fg); _percent.draw(p, { .position = percent.topLeft(), }); @@ -948,6 +979,7 @@ AttributesList::AttributesList( QWidget *parent, not_null delegate, not_null attributes, + std::vector otherModels, rpl::producer tab, Selection initialSelection) : BoxContentDivider(parent) @@ -958,6 +990,14 @@ AttributesList::AttributesList( , _entries(&_models) , _list(&_entries->list) { _singleMin = _delegate->buttonSize(); + _activeModels.assign( + attributes->models.begin(), + attributes->models.end()); + _otherModels = std::move(otherModels); + + _craftedModels = !_activeModels.empty() + && (_activeModels.front().rarityType() + != Data::UniqueGiftRarity::Default); fill(); @@ -972,8 +1012,8 @@ AttributesList::AttributesList( Unexpected("Tab in AttributesList."); }(); _list = &_entries->list; - refreshButtons(); refreshAbout(); + refreshButtons(); }, lifetime()); _selected.value() | rpl::combine_previous() | rpl::on_next([=]( @@ -1008,6 +1048,11 @@ auto AttributesList::selected() const -> rpl::producer { return _selected.value(); } +auto AttributesList::modelsToggled() const + -> rpl::producer> { + return _modelsToggled.events(); +} + void AttributesList::fill() { const auto addEntries = [&]( const auto &source, @@ -1021,7 +1066,7 @@ void AttributesList::fill() { target.list.emplace_back(Entry{ { item }, { value } }); } }; - addEntries(_attributes->models, _models, &Selection::model); + addEntries(_activeModels, _models, &Selection::model); addEntries(_attributes->patterns, _patterns, &Selection::pattern); addEntries(_attributes->backdrops, _backdrops, &Selection::backdrop); } @@ -1186,7 +1231,9 @@ void AttributesList::clicked(int index) { void AttributesList::refreshAbout() { auto text = [&] { switch (_tab.current()) { - case Tab::Model: return tr::lng_auction_preview_models; + case Tab::Model: return _craftedModels + ? tr::lng_auction_preview_crafted + : tr::lng_auction_preview_models; case Tab::Pattern: return tr::lng_auction_preview_symbols; case Tab::Backdrop: return tr::lng_auction_preview_backdrops; } @@ -1198,9 +1245,42 @@ void AttributesList::refreshAbout() { st::giftListAbout); about->show(); _about = std::move(about); + + if (_tab.current() == Tab::Model && !_otherModels.empty()) { + auto linkText = (_craftedModels + ? tr::lng_auction_preview_view_primary + : tr::lng_auction_preview_view_craftable)( + lt_arrow, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + tr::link); + auto link = std::make_unique( + this, + std::move(linkText), + st::giftListAboutToggle); + link->overrideLinkClickHandler([=] { toggleModels(); }); + link->show(); + _toggleLink = std::move(link); + } else { + _toggleLink = nullptr; + } + resizeToWidth(width()); } +void AttributesList::toggleModels() { + std::swap(_activeModels, _otherModels); + _craftedModels = !_craftedModels; + fill(); + _entries = &_models; + _list = &_entries->list; + _modelsToggled.fire_copy(_activeModels); + auto sel = _selected.current(); + sel.model = -1; + _selected.force_assign(sel); + refreshAbout(); + refreshButtons(); +} + int AttributesList::resizeGetHeight(int width) { const auto padding = st::giftBoxPadding; const auto count = int(_list->size()); @@ -1215,9 +1295,25 @@ int AttributesList::resizeGetHeight(int width) { auto result = 0; const auto margin = st::giftListAboutMargin; - _about->resizeToWidth(width - margin.left() - margin.right()); - _about->moveToLeft(margin.left(), result + margin.top()); - result += margin.top() + _about->height() + margin.bottom(); + const auto contentWidth = width - margin.left() - margin.right(); + _about->resizeToWidth(contentWidth); + if (_toggleLink) { + _toggleLink->resizeToWidth(contentWidth); + } + const auto addedSpace = _toggleLink + ? (st::giftListAboutToggleSkip + _toggleLink->height()) + : 0; + const auto marginTop = margin.top() - addedSpace / 2; + const auto marginBottom = margin.bottom() + - (addedSpace - addedSpace / 2); + _about->moveToLeft(margin.left(), result + marginTop); + result += marginTop + _about->height(); + if (_toggleLink) { + result += st::giftListAboutToggleSkip; + _toggleLink->moveToLeft(margin.left(), result); + result += _toggleLink->height(); + } + result += marginBottom; const auto singlew = std::min( ((available + skipw) / _perRow) - skipw, @@ -1241,13 +1337,64 @@ int AttributesList::resizeGetHeight(int width) { void StarGiftPreviewBox( not_null box, const QString &title, - const Data::UniqueGiftAttributes &attributes, + Data::UniqueGiftAttributes attributes, Data::GiftAttributeIdType tab, std::shared_ptr selected) { Expects(!attributes.models.empty()); Expects(!attributes.patterns.empty()); Expects(!attributes.backdrops.empty()); + const auto filter = [&](auto &list, auto attribute) { + const auto rarityProj = &Data::UniqueGiftAttribute::rarityType; + const auto simple = Data::UniqueGiftRarity::Default; + if (!selected || ((*selected).*attribute).rarityType() == simple) { + if (ranges::contains(list, simple, rarityProj)) { + list.erase(ranges::remove_if(list, [&](const auto &attribute) { + return attribute.rarityType() != simple; + }), end(list)); + } + } else { + const auto rare = ((*selected).*attribute).rarityType(); + if (ranges::contains(list, rare, rarityProj)) { + list.erase(ranges::remove_if(list, [&](const auto &attribute) { + return attribute.rarityType() == simple; + }), end(list)); + } + } + Assert(!list.empty()); + }; + filter(attributes.patterns, &Data::UniqueGift::pattern); + filter(attributes.backdrops, &Data::UniqueGift::backdrop); + + auto otherModels = std::vector(); + { + const auto simple = Data::UniqueGiftRarity::Default; + const auto showCrafted = selected + && (selected->model.rarityType() != simple); + auto defaultModels = std::vector(); + auto craftedModels = std::vector(); + for (auto &model : attributes.models) { + if (model.rarityType() == simple) { + defaultModels.push_back(std::move(model)); + } else { + craftedModels.push_back(std::move(model)); + } + } + if (!defaultModels.empty() && !craftedModels.empty()) { + if (showCrafted) { + attributes.models = std::move(craftedModels); + otherModels = std::move(defaultModels); + } else { + attributes.models = std::move(defaultModels); + otherModels = std::move(craftedModels); + } + } else if (!defaultModels.empty()) { + attributes.models = std::move(defaultModels); + } else { + attributes.models = std::move(craftedModels); + } + } + box->setStyle(st::uniqueAttributesBox); box->setWidth(st::boxWideWidth); box->setNoContentMargin(true); @@ -1383,6 +1530,7 @@ void StarGiftPreviewBox( box, &state->delegate, &state->attributes, + std::move(otherModels), state->tab.value(), state->fixed)); state->list->selected( @@ -1393,6 +1541,11 @@ void StarGiftPreviewBox( || (value.backdrop >= 0); state->push(); }, box->lifetime()); + state->list->modelsToggled( + ) | rpl::on_next([=](std::vector models) { + state->attributes.models = std::move(models); + state->models.clear(); + }, box->lifetime()); const auto button = box->addButton(rpl::single(u""_q), [] {}); const auto buttonsParent = button->parentWidget(); diff --git a/Telegram/SourceFiles/boxes/star_gift_preview_box.h b/Telegram/SourceFiles/boxes/star_gift_preview_box.h index 0deed006fd..be3c065608 100644 --- a/Telegram/SourceFiles/boxes/star_gift_preview_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_preview_box.h @@ -25,7 +25,7 @@ class GenericBox; void StarGiftPreviewBox( not_null box, const QString &title, - const Data::UniqueGiftAttributes &attributes, + Data::UniqueGiftAttributes attributes, Data::GiftAttributeIdType tab, std::shared_ptr selected); diff --git a/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp b/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp index 0c1cdf84dd..bd1190c529 100644 --- a/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_resale_box.cpp @@ -8,12 +8,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/star_gift_resale_box.h" #include "boxes/star_gift_box.h" +#include "boxes/transfer_gift_box.h" #include "chat_helpers/compose/compose_show.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_star_gift.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "info/peer_gifts/info_peer_gifts_common.h" #include "main/main_session.h" @@ -23,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" +#include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "window/window_session_controller.h" @@ -81,7 +84,6 @@ struct ResaleTabs { }; [[nodiscard]] ResaleTabs MakeResaleTabs( std::shared_ptr show, - not_null peer, const ResaleGiftsDescriptor &info, rpl::producer filter) { auto widget = object_ptr((QWidget*)nullptr); @@ -107,9 +109,29 @@ struct ResaleTabs { }; const auto state = raw->lifetime().make_state(); state->filter = std::move(filter); + + const auto forCraft = state->filter.current().forCraft; + const auto wrapName = [=](QString name, int count) { + auto result = tr::marked(name); + if (!forCraft) { + result.append(' ').append(tr::bold( + Lang::FormatCountDecimal(count))); + } + return result; + }; + state->lists.backdrops = info.backdrops; state->lists.models = info.models; state->lists.patterns = info.patterns; + if (forCraft) { + using namespace Data; + const auto isRare = [](const UniqueGiftModelCount &entry) { + return entry.model.rarityType() != UniqueGiftRarity::Default; + }; + state->lists.models.erase( + ranges::remove_if(state->lists.models, isRare), + end(state->lists.models)); + } const auto scroll = [=] { return QPoint(int(base::SafeRound(state->scroll)), 0); @@ -263,11 +285,9 @@ struct ResaleTabs { if (type == GiftAttributeIdType::Model) { for (auto &entry : state->lists.models) { const auto id = IdFor(entry.model); - const auto text = TextWithEntities{ - entry.model.name - }.append(' ').append(tr::bold( - Lang::FormatCountDecimal(entry.count) - )); + const auto text = wrapName( + entry.model.name, + entry.count); actionWithDocument(text, [=] { toggle(id); }, id.value, checked(id)); @@ -275,11 +295,9 @@ struct ResaleTabs { } else if (type == GiftAttributeIdType::Backdrop) { for (auto &entry : state->lists.backdrops) { const auto id = IdFor(entry.backdrop); - const auto text = TextWithEntities{ - entry.backdrop.name - }.append(' ').append(tr::bold( - Lang::FormatCountDecimal(entry.count) - )); + const auto text = wrapName( + entry.backdrop.name, + entry.count); actionWithColor(text, [=] { toggle(id); }, entry.backdrop.centerColor, checked(id)); @@ -287,11 +305,9 @@ struct ResaleTabs { } else if (type == GiftAttributeIdType::Pattern) { for (auto &entry : state->lists.patterns) { const auto id = IdFor(entry.pattern); - const auto text = TextWithEntities{ - entry.pattern.name - }.append(' ').append(tr::bold( - Lang::FormatCountDecimal(entry.count) - )); + const auto text = wrapName( + entry.pattern.name, + entry.count); actionWithDocument(text, [=] { toggle(id); }, id.value, checked(id)); @@ -470,16 +486,13 @@ void GiftResaleBox( ResaleGiftsDescriptor descriptor) { box->setWidth(st::boxWideWidth); - // Create a proper vertical layout for the title const auto titleWrap = box->setPinnedToTopContent( object_ptr(box.get())); - // Add vertical spacing above the title titleWrap->add(object_ptr( titleWrap, st::defaultVerticalListSkip)); - // Add the gift name with semibold style titleWrap->add( object_ptr( titleWrap, @@ -487,7 +500,6 @@ void GiftResaleBox( st::boxTitle), QMargins(st::boxRowPadding.left(), 0, st::boxRowPadding.right(), 0)); - // Add the count text in gray below with proper translation const auto countLabel = titleWrap->add( object_ptr( titleWrap, @@ -506,22 +518,16 @@ void GiftResaleBox( }, content->lifetime()); struct State { - rpl::event_stream<> updated; - ResaleGiftsDescriptor data; - rpl::variable filter; rpl::variable ton; - rpl::lifetime loading; int lastMinHeight = 0; }; const auto state = content->lifetime().make_state(); - state->data = std::move(descriptor); box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); }); #ifndef OS_MAC_STORE const auto currency = box->addLeftButton(rpl::single(QString()), [=] { state->ton = !state->ton.current(); - state->updated.fire({}); }); currency->setText(rpl::conditional( state->ton.value(), @@ -536,18 +542,62 @@ void GiftResaleBox( } }, content->lifetime()); + AddResaleGiftsList( + window, + peer, + content, + std::move(descriptor), + state->ton.value()); +} + +} // namespace + +void AddResaleGiftsList( + not_null window, + not_null peer, + not_null container, + Data::ResaleGiftsDescriptor descriptor, + rpl::producer forceTon, + Fn)> bought, + bool forCraft) { + struct State { + rpl::event_stream<> updated; + ResaleGiftsDescriptor data; + rpl::variable filter; + rpl::variable ton; + rpl::variable empty = true; + rpl::lifetime loading; + int lastMinHeight = 0; + }; + const auto state = container->lifetime().make_state(); + state->filter = ResaleGiftsFilter{ .forCraft = forCraft }; + state->data = std::move(descriptor); + state->ton = std::move(forceTon); + auto tabs = MakeResaleTabs( window->uiShow(), - peer, state->data, state->filter.value()); state->filter = std::move(tabs.filter); - content->add(std::move(tabs.widget)); + if (forCraft) { + const auto skip = st::giftBoxResaleTabsMargin.top() + - st::giftBoxTabsMargin.bottom(); + const auto wrap = container->add(object_ptr>( + container, + std::move(tabs.widget), + QMargins(0, 0, 0, skip))); + wrap->paintOn([=](QPainter &p) { + p.fillRect(wrap->rect(), st::windowBgOver); + }); + } else { + container->add(std::move(tabs.widget)); + } + const auto session = &window->session(); state->filter.changes() | rpl::on_next([=](ResaleGiftsFilter value) { state->data.offset = QString(); state->loading = ResaleGiftsSlice( - &peer->session(), + session, state->data.giftId, value, QString() @@ -559,9 +609,9 @@ void GiftResaleBox( state->data.list = std::move(slice.list); state->updated.fire({}); }); - }, content->lifetime()); + }, container->lifetime()); - peer->owner().giftUpdates( + session->data().giftUpdates( ) | rpl::on_next([=](const Data::GiftUpdate &update) { using Action = Data::GiftUpdate::Action; const auto action = update.action; @@ -581,26 +631,52 @@ void GiftResaleBox( state->data.list.erase(i); } state->updated.fire({}); - }, box->lifetime()); + }, container->lifetime()); - content->add(MakeGiftsSendList(window, peer, rpl::single( + using Descriptor = Info::PeerGifts::GiftDescriptor; + auto customHandler = Fn(); + if (bought) { + using StarGift = Info::PeerGifts::GiftTypeStars; + customHandler = crl::guard(container, [=](Descriptor descriptor) { + Expects(v::is(descriptor)); + + const auto unique = v::get(descriptor).info.unique; + const auto done = crl::guard(container, [=](bool ok) { + if (ok) { + bought(unique); + } + }); + const auto to = peer->session().user(); + const auto ton = state->ton.current(); + ShowBuyResaleGiftBox(window->uiShow(), unique, ton, to, done); + }); + } + + auto gifts = rpl::single( rpl::empty - ) | rpl::then( - state->updated.events() - ) | rpl::map([=] { + ) | rpl::then(rpl::merge( + state->updated.events() | rpl::type_erased, + state->ton.changes() | rpl::to_empty | rpl::type_erased + )) | rpl::map([=] { auto result = GiftsDescriptor(); const auto selfId = window->session().userPeerId(); const auto forceTon = state->ton.current(); for (const auto &gift : state->data.list) { + const auto mine = (gift.unique->ownerId == selfId); + if (mine && forCraft) { + continue; + } result.list.push_back(Info::PeerGifts::GiftTypeStars{ .info = gift, .forceTon = forceTon, .resale = true, - .mine = (gift.unique->ownerId == selfId), - }); + .mine = mine, + }); } + state->empty = result.list.empty(); return result; - }), [=] { + }); + const auto loadMore = [=] { if (!state->data.offset.isEmpty() && !state->loading) { state->loading = ResaleGiftsSlice( @@ -620,17 +696,48 @@ void GiftResaleBox( state->updated.fire({}); }); } + }; + container->add(MakeGiftsList({ + .window = window, + .mode = forCraft ? GiftsListMode::CraftResale : GiftsListMode::Send, + .peer = peer, + .gifts = std::move(gifts), + .loadMore = loadMore, + .handler = customHandler, })); -} -} // namespace + const auto skip = st::defaultSubsectionTitlePadding.top(); + const auto wrap = container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_gift_craft_search_none(), + st::craftYourListEmpty), + (st::boxRowPadding + QMargins(0, 0, 0, skip))), + style::al_top); + state->empty.value() | rpl::on_next([=](bool empty) { + // Scroll doesn't jump up if we show before rows are cleared, + // and we hide after rows are added. + if (empty) { + wrap->show(anim::type::instant); + } else { + crl::on_main(wrap, [=] { + if (!state->empty.current()) { + wrap->hide(anim::type::instant); + } + }); + } + }, wrap->lifetime()); + wrap->entity()->setTryMakeSimilarLines(true); +} void ShowResaleGiftBoughtToast( std::shared_ptr show, not_null to, const Data::UniqueGift &gift) { show->showToast({ - .title = tr::lng_gift_sent_title(tr::now), + .title = to->isSelf() ? QString() : tr::lng_gift_sent_title(tr::now), .text = TextWithEntities{ (to->isSelf() ? tr::lng_gift_sent_resale_done_self( tr::now, diff --git a/Telegram/SourceFiles/boxes/star_gift_resale_box.h b/Telegram/SourceFiles/boxes/star_gift_resale_box.h index fb856a8c0a..ef45be3f8d 100644 --- a/Telegram/SourceFiles/boxes/star_gift_resale_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_resale_box.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct UniqueGift; +struct ResaleGiftsDescriptor; } // namespace Data namespace Info::PeerGifts { @@ -26,6 +27,8 @@ class SessionController; namespace Ui { +class VerticalLayout; + void ShowResaleGiftBoughtToast( std::shared_ptr show, not_null to, @@ -38,4 +41,13 @@ void ShowResaleGiftBoughtToast( QString title, Fn finishRequesting); +void AddResaleGiftsList( + not_null window, + not_null peer, + not_null container, + Data::ResaleGiftsDescriptor descriptor, + rpl::producer forceTon, + Fn)> bought = nullptr, + bool forCraft = false); + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 5d2b6a6ba0..18edd80797 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -33,7 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/clip/media_clip_reader.h" #include "menu/menu_send.h" #include "mtproto/sender.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "storage/storage_account.h" #include "ui/boxes/confirm_box.h" #include "ui/cached_round_corners.h" @@ -56,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" +#include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_chat_helpers.h" #include "styles/style_info.h" @@ -296,6 +297,7 @@ public: [[nodiscard]] uint64 setId() const; void install(); + void showPreviewForDocument(DocumentId documentId); [[nodiscard]] rpl::producer setInstalled() const; [[nodiscard]] rpl::producer setArchived() const; [[nodiscard]] rpl::producer<> updateControls() const; @@ -474,6 +476,8 @@ private: base::Timer _previewTimer; int _previewShown = -1; + DocumentId _previewDocumentId = 0; + bool _previewLocked = false; base::unique_qptr _menu; @@ -488,11 +492,13 @@ StickerSetBox::StickerSetBox( QWidget *parent, std::shared_ptr show, const StickerSetIdentifier &set, - Data::StickersType type) + Data::StickersType type, + DocumentId previewDocumentId) : _show(std::move(show)) , _session(&_show->session()) , _set(set) -, _type(type) { +, _type(type) +, _previewDocumentId(previewDocumentId) { } StickerSetBox::StickerSetBox( @@ -504,13 +510,15 @@ StickerSetBox::StickerSetBox( base::weak_qptr StickerSetBox::Show( std::shared_ptr show, - not_null document) { + not_null document, + DocumentId previewDocumentId) { if (const auto sticker = document->sticker()) { if (sticker->set) { auto box = Box( show, sticker->set, - sticker->setType); + sticker->setType, + previewDocumentId); const auto result = base::make_weak(box.data()); show->showBox(std::move(box)); return result; @@ -525,6 +533,9 @@ void StickerSetBox::prepare() { _inner = setInnerWidget( object_ptr(this, _show, _set, _type), st::stickersScroll); + if (const auto previewId = base::take(_previewDocumentId)) { + _inner->showPreviewForDocument(previewId); + } _session->data().stickers().updated( _type ) | rpl::on_next([=] { @@ -595,6 +606,13 @@ void StickerSetBox::prepare() { closeBox(); }, lifetime()); + + boxClosing( + ) | rpl::on_next([show = _show] { + if (const auto window = show->resolveWindow()) { + window->widget()->hideMediaPreview(); + } + }, lifetime()); } void StickerSetBox::addStickers() { @@ -1103,6 +1121,9 @@ void StickerSetBox::Inner::applySet(const TLStickerSet &set) { _padding.top() + _rowsCount * _singleSize.height() + _padding.bottom()); _loaded = true; + if (const auto previewId = base::take(_previewDocumentId)) { + showPreviewForDocument(previewId); + } updateSelected(); _updateControls.fire({}); } @@ -1219,6 +1240,14 @@ void StickerSetBox::Inner::installDone( } void StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) { + if (_previewLocked) { + _previewLocked = false; + _previewShown = -1; + if (const auto window = _show->resolveWindow()) { + window->widget()->hideMediaPreview(); + } + return; + } if (e->button() != Qt::LeftButton) { return; } @@ -1320,7 +1349,7 @@ void StickerSetBox::Inner::mouseMoveEvent(QMouseEvent *e) { } update(); } - if (_previewShown >= 0) { + if (_previewShown >= 0 && !_previewLocked) { showPreviewAt(e->globalPos()); } } @@ -1337,6 +1366,27 @@ void StickerSetBox::Inner::showPreviewAt(QPoint globalPos) { } } +void StickerSetBox::Inner::showPreviewForDocument(DocumentId documentId) { + if (!_loaded) { + _previewDocumentId = documentId; + return; + } + const auto it = ranges::find( + _pack, + documentId, + &DocumentData::id); + if (it != _pack.end()) { + const auto index = int(it - _pack.begin()); + if (index != _previewShown) { + _previewShown = index; + _previewLocked = true; + _show->showMediaPreview( + Data::FileOriginStickerSet(_setId, _setAccessHash), + _pack[index]); + } + } +} + void StickerSetBox::Inner::leaveEventHook(QEvent *e) { setSelected(-1); } @@ -1422,6 +1472,12 @@ void StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) { kStickerMoveDuration); } if (_previewShown >= 0) { + if (_previewLocked) { + _previewLocked = false; + if (const auto window = _show->resolveWindow()) { + window->widget()->hideMediaPreview(); + } + } _previewShown = -1; return; } diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index 85fbc2a404..b4a88ce930 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -64,7 +64,8 @@ public: QWidget*, std::shared_ptr show, const StickerSetIdentifier &set, - Data::StickersType type); + Data::StickersType type, + DocumentId previewDocumentId = 0); StickerSetBox( QWidget*, std::shared_ptr show, @@ -72,7 +73,8 @@ public: static base::weak_qptr Show( std::shared_ptr show, - not_null document); + not_null document, + DocumentId previewDocumentId = 0); protected: void prepare() override; @@ -97,5 +99,6 @@ private: class Inner; QPointer _inner; + DocumentId _previewDocumentId = 0; }; diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 4e59808196..ed4a7d02c7 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -750,28 +750,28 @@ void StickersBox::refreshTabs() { sections.push_back(tr::lng_stickers_masks_tab(tr::now)); _tabIndices.push_back(Section::Masks); } - if (!stickers.featuredSetsOrder().isEmpty() && _featured.widget()) { + const auto showFeatured = _featured.widget() + && (!stickers.featuredSetsOrder().isEmpty() + || _section == Section::Featured); + if (showFeatured) { sections.push_back(tr::lng_stickers_featured_tab(tr::now)); _tabIndices.push_back(Section::Featured); } - if (!archivedSetsOrder().isEmpty() && _archived.widget()) { + const auto showArchived = _archived.widget() + && (!archivedSetsOrder().isEmpty() + || _section == Section::Archived); + if (showArchived) { sections.push_back(tr::lng_stickers_archived_tab(tr::now)); _tabIndices.push_back(Section::Archived); } _tabs->setSections(sections); - if ((_tab == &_archived && !_tabIndices.contains(Section::Archived)) - || (_tab == &_featured && !_tabIndices.contains(Section::Featured)) - || (_tab == &_masks && !_tabIndices.contains(Section::Masks))) { + if ((_section == Section::Archived && !_tabIndices.contains(Section::Archived)) + || (_section == Section::Featured && !_tabIndices.contains(Section::Featured)) + || (_section == Section::Masks && !_tabIndices.contains(Section::Masks))) { switchTab(); } else { _ignoreTabActivation = true; - _tabs->setActiveSectionFast(_tabIndices.indexOf((_tab == &_archived) - ? Section::Archived - : (_tab == &_featured) - ? Section::Featured - : (_tab == &_masks) - ? Section::Masks - : Section::Installed)); + _tabs->setActiveSectionFast(_tabIndices.indexOf(_section)); _ignoreTabActivation = false; } updateTabsGeometry(); diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp index 2165bd9f94..032d8a1dfb 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp @@ -526,9 +526,11 @@ void BuyResaleGift( auto paymentDone = [=]( Payments::CheckoutResult result, const MTPUpdates *updates) { - done(result); if (result == Payments::CheckoutResult::Paid) { gift->starsForResale = 0; + } + done(result); + if (result == Payments::CheckoutResult::Paid) { to->owner().notifyGiftUpdate({ .slug = gift->slug, .action = Data::GiftUpdate::Action::ResaleChange, diff --git a/Telegram/SourceFiles/boxes/url_auth_box.cpp b/Telegram/SourceFiles/boxes/url_auth_box.cpp index 157b3274b3..c6e8085199 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box.cpp @@ -7,24 +7,135 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/url_auth_box.h" -#include "boxes/abstract_box.h" -#include "history/history.h" -#include "history/history_item.h" -#include "history/history_item_components.h" +#include "apiwrap.h" +#include "boxes/url_auth_box_content.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "data/data_peer.h" #include "data/data_session.h" #include "data/data_user.h" -#include "core/click_handler_types.h" -#include "ui/text/text_utilities.h" -#include "ui/wrap/vertical_layout.h" -#include "ui/widgets/checkbox.h" -#include "ui/widgets/labels.h" +#include "history/history_item_components.h" +#include "history/history_item.h" +#include "history/history.h" +#include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_domain.h" #include "main/main_session.h" -#include "apiwrap.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/userpic_button.h" +#include "ui/layers/generic_box.h" +#include "ui/toast/toast.h" +#include "ui/widgets/menu/menu_action.h" +#include "ui/widgets/popup_menu.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" +#include "styles/style_settings.h" +#include "styles/style_premium.h" +#include "styles/style_window.h" -void UrlAuthBox::Activate( +namespace UrlAuthBox { +namespace { + +using AnotherSessionFactory = Fn()>; + +struct SwitchAccountResult { + not_null widget; + AnotherSessionFactory anotherSession; +}; + +[[nodiscard]] SwitchAccountResult AddAccountsMenu( + not_null parent) { + const auto session = &Core::App().domain().active().session(); + const auto widget = Ui::CreateChild( + parent, + st::restoreUserpicIcon.photoSize + st::lineWidth * 8); + struct State { + base::unique_qptr menu; + UserData *currentUser = nullptr; + }; + const auto state = widget->lifetime().make_state(); + state->currentUser = session->user(); + const auto userpic = Ui::CreateChild( + parent, + state->currentUser, + st::restoreUserpicIcon); + widget->setUserpic(userpic); + const auto isCurrentTest = session->isTestMode(); + const auto filtered = [=] { + auto result = std::vector>(); + for (const auto &account : Core::App().domain().orderedAccounts()) { + if (!account->sessionExists() + || (account->session().user() == state->currentUser) + || (account->session().isTestMode() != isCurrentTest)) { + continue; + } + result.push_back(&account->session()); + } + return result; + }; + const auto isSingle = filtered().empty(); + widget->setExpanded(!isSingle); + widget->setAttribute(Qt::WA_TransparentForMouseEvents, isSingle); + widget->setClickedCallback([=] { + const auto &st = st::popupMenuWithIcons; + state->menu = base::make_unique_q(widget, st); + for (const auto &anotherSession : filtered()) { + const auto user = anotherSession->user(); + const auto action = new QAction(user->name(), state->menu); + QObject::connect(action, &QAction::triggered, [=] { + state->currentUser = user; + const auto newUserpic = Ui::CreateChild( + parent, + user, + st::restoreUserpicIcon); + widget->setUserpic(newUserpic); + }); + auto owned = base::make_unique_q( + state->menu->menu(), + state->menu->menu()->st(), + action, + nullptr, + nullptr); + const auto menuUserpic = Ui::CreateChild( + owned.get(), + user, + st::lockSetupEmailUserpicSmall); + menuUserpic->setAttribute(Qt::WA_TransparentForMouseEvents); + menuUserpic->move(st.menu.itemIconPosition); + state->menu->addAction(std::move(owned)); + } + + state->menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + state->menu->popup( + widget->mapToGlobal( + QPoint( + widget->width() + st.shadow.extend.right(), + widget->height()))); + }); + return { + widget, + [=] { return &state->currentUser->session(); }, + }; +} + +} // namespace + +void RequestButton( + std::shared_ptr show, + const MTPDurlAuthResultRequest &request, + not_null message, + int row, + int column); +void RequestUrl( + std::shared_ptr show, + const MTPDurlAuthResultRequest &request, + not_null session, + const QString &url, + QVariant context); + +void ActivateButton( + std::shared_ptr show, not_null message, int row, int column) { @@ -61,12 +172,14 @@ void UrlAuthBox::Activate( button->requestId = 0; result.match([&](const MTPDurlAuthResultAccepted &data) { - UrlClickHandler::Open(qs(data.vurl())); + if (const auto url = data.vurl()) { + UrlClickHandler::Open(qs(url->v)); + } }, [&](const MTPDurlAuthResultDefault &data) { HiddenUrlClickHandler::Open(url); }, [&](const MTPDurlAuthResultRequest &data) { if (const auto item = session->data().message(itemId)) { - Request(data, item, row, column); + RequestButton(show, data, item, row, column); } }); }).fail([=] { @@ -75,14 +188,17 @@ void UrlAuthBox::Activate( itemId, row, column); - if (!button) return; + if (!button) { + return; + } button->requestId = 0; HiddenUrlClickHandler::Open(url); }).send(); } -void UrlAuthBox::Activate( +void ActivateUrl( + std::shared_ptr show, not_null session, const QString &url, QVariant context) { @@ -101,18 +217,24 @@ void UrlAuthBox::Activate( MTP_string(url) )).done([=](const MTPUrlAuthResult &result) { result.match([&](const MTPDurlAuthResultAccepted &data) { - UrlClickHandler::Open(qs(data.vurl()), context); + UrlClickHandler::Open(qs(data.vurl().value_or_empty()), context); }, [&](const MTPDurlAuthResultDefault &data) { HiddenUrlClickHandler::Open(url, context); }, [&](const MTPDurlAuthResultRequest &data) { - Request(data, session, url, context); + RequestUrl(show, data, session, url, context); }); - }).fail([=] { - HiddenUrlClickHandler::Open(url, context); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"URL_EXPIRED"_q) { + show->showToast( + tr::lng_url_auth_phone_toast_bad_expired(tr::now)); + } else { + HiddenUrlClickHandler::Open(url, context); + } }).send(); } -void UrlAuthBox::Request( +void RequestButton( + std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null message, int row, @@ -123,7 +245,7 @@ void UrlAuthBox::Request( itemId, row, column); - if (button->requestId || !message->isRegular()) { + if (!button || button->requestId || !message->isRegular()) { return; } const auto session = &message->history()->session(); @@ -135,19 +257,24 @@ void UrlAuthBox::Request( ? session->data().processUser(request.vbot()).get() : nullptr; const auto box = std::make_shared>(); - const auto finishWithUrl = [=](const QString &url) { + const auto finishWithUrl = [=](const QString &url, bool accepted) { if (*box) { (*box)->closeBox(); } - UrlClickHandler::Open(url); + if (url.isEmpty() && accepted) { + show->showToast(tr::lng_passport_success(tr::now)); + } else { + UrlClickHandler::Open(url); + } }; const auto callback = [=](Result result) { - if (result == Result::None) { - finishWithUrl(url); + if (!result.auth) { + finishWithUrl(url, false); } else if (session->data().message(itemId)) { - const auto allowWrite = (result == Result::AuthAndAllowWrite); using Flag = MTPmessages_AcceptUrlAuth::Flag; - const auto flags = (allowWrite ? Flag::f_write_allowed : Flag(0)) + const auto flags = Flag(0) + | (result.allowWrite ? Flag::f_write_allowed : Flag(0)) + | (result.sharePhone ? Flag::f_share_phone_number : Flag(0)) | (Flag::f_peer | Flag::f_msg_id | Flag::f_button_id); session->api().request(MTPmessages_AcceptUrlAuth( MTP_flags(flags), @@ -156,9 +283,15 @@ void UrlAuthBox::Request( MTP_int(buttonId), MTPstring() // #TODO auth url )).done([=](const MTPUrlAuthResult &result) { + const auto accepted = result.match( + [](const MTPDurlAuthResultAccepted &data) { + return true; + }, [](const auto &) { + return false; + }); const auto to = result.match( [&](const MTPDurlAuthResultAccepted &data) { - return qs(data.vurl()); + return qs(data.vurl().value_or_empty()); }, [&](const MTPDurlAuthResultDefault &data) { return url; }, [&](const MTPDurlAuthResultRequest &data) { @@ -166,18 +299,25 @@ void UrlAuthBox::Request( "got urlAuthResultRequest after acceptUrlAuth.")); return url; }); - finishWithUrl(to); + finishWithUrl(to, accepted); }).fail([=] { - finishWithUrl(url); + finishWithUrl(url, false); }).send(); } }; - *box = Ui::show( - Box(session, url, qs(request.vdomain()), bot, callback), + *box = show->show( + Box( + Show, + url, + qs(request.vdomain()), + session->user()->name(), + bot->firstName, + callback), Ui::LayerOption::KeepOther); } -void UrlAuthBox::Request( +void RequestUrl( + std::shared_ptr show, const MTPDurlAuthResultRequest &request, not_null session, const QString &url, @@ -185,31 +325,49 @@ void UrlAuthBox::Request( 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 domain = qs(request.vdomain()); const auto box = std::make_shared>(); - const auto finishWithUrl = [=](const QString &url) { + const auto finishWithUrl = [=](const QString &url, bool accepted) { if (*box) { (*box)->closeBox(); } - UrlClickHandler::Open(url, context); - }; - const auto callback = [=](Result result) { - if (result == Result::None) { - finishWithUrl(url); + + if (url.isEmpty() && accepted) { } else { - const auto allowWrite = (result == Result::AuthAndAllowWrite); + UrlClickHandler::Open(url, context); + } + }; + const auto anotherSessionFactory + = std::make_shared(nullptr); + const auto sendRequest = [=](Result result) { + if (!result.auth) { + finishWithUrl(url, false); + } else { + const auto sharePhone = result.sharePhone; using Flag = MTPmessages_AcceptUrlAuth::Flag; - const auto flags = (allowWrite ? Flag::f_write_allowed : Flag(0)) - | Flag::f_url; - session->api().request(MTPmessages_AcceptUrlAuth( + 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( MTP_flags(flags), MTPInputPeer(), MTPint(), // msg_id MTPint(), // button_id MTP_string(url) )).done([=](const MTPUrlAuthResult &result) { + const auto accepted = result.match( + [](const MTPDurlAuthResultAccepted &data) { + return true; + }, [](const auto &) { + return false; + }); const auto to = result.match( [&](const MTPDurlAuthResultAccepted &data) { - return qs(data.vurl()); + return qs(data.vurl().value_or_empty()); }, [&](const MTPDurlAuthResultDefault &data) { return url; }, [&](const MTPDurlAuthResultRequest &data) { @@ -217,97 +375,99 @@ void UrlAuthBox::Request( "got urlAuthResultRequest after acceptUrlAuth.")); return url; }); - finishWithUrl(to); + finishWithUrl(to, accepted); + show->showToast(Ui::Toast::Config{ + .title = tr::lng_url_auth_phone_toast_good_title(tr::now), + .text = ((requestPhone && !sharePhone) + ? tr::lng_url_auth_phone_toast_good_no_phone + : tr::lng_url_auth_phone_toast_good)( + tr::now, + lt_domain, + tr::link(domain), + tr::marked), + .duration = crl::time(4000), + }); }).fail([=] { - finishWithUrl(url); + show->showToast(Ui::Toast::Config{ + .title = tr::lng_url_auth_phone_toast_bad_title(tr::now), + .text = tr::lng_url_auth_phone_toast_bad( + tr::now, + lt_domain, + tr::link(domain), + tr::marked), + .duration = crl::time(4000), + }); + finishWithUrl(url, false); }).send(); } }; - *box = Ui::show( - Box(session, url, qs(request.vdomain()), bot, callback), - Ui::LayerOption::KeepOther); -} - -UrlAuthBox::UrlAuthBox( - QWidget*, - not_null session, - const QString &url, - const QString &domain, - UserData *bot, - Fn callback) -: _content(setupContent(session, url, domain, bot, std::move(callback))) { -} - -void UrlAuthBox::prepare() { - setDimensionsToContent(st::boxWidth, _content); - addButton(tr::lng_open_link(), [=] { _callback(); }); - addButton(tr::lng_cancel(), [=] { closeBox(); }); -} - -not_null UrlAuthBox::setupContent( - not_null session, - const QString &url, - const QString &domain, - UserData *bot, - Fn callback) { - const auto result = Ui::CreateChild(this); - result->add( - object_ptr( - result, - tr::lng_url_auth_open_confirm(tr::now, lt_link, url), - st::boxLabel), - st::boxPadding); - const auto addCheckbox = [&](const TextWithEntities &text) { - const auto checkbox = result->add( - object_ptr( - result, - text, - true, - st::urlAuthCheckbox), - style::margins( - st::boxPadding.left(), - st::boxPadding.bottom(), - st::boxPadding.right(), - st::boxPadding.bottom())); - checkbox->setAllowTextLines(); - return checkbox; - }; - const auto auth = addCheckbox( - tr::lng_url_auth_login_option( - tr::now, - lt_domain, - tr::bold(domain), - lt_user, - tr::bold(session->user()->name()), - tr::marked)); - const auto allow = bot - ? addCheckbox(tr::lng_url_auth_allow_messages( - tr::now, - lt_bot, - tr::bold(bot->firstName), - tr::marked)) - : nullptr; - if (allow) { - rpl::single( - auth->checked() - ) | rpl::then( - auth->checkedChanges() - ) | rpl::on_next([=](bool checked) { - if (!checked) { - allow->setChecked(false); + const auto browser = qs(request.vbrowser().value_or("Unknown browser")); + 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 box) { + const auto callback = [=](Result result) { + if (!requestPhone) { + return sendRequest(result); } - allow->setDisabled(!checked); - }, auth->lifetime()); - } - _callback = [=, callback = std::move(callback)]() { - const auto authed = auth->checked(); - const auto allowed = (authed && allow && allow->checked()); - const auto onstack = callback; - onstack(allowed - ? Result::AuthAndAllowWrite - : authed - ? Result::Auth - : Result::None); - }; - return result; + box->uiShow()->show(Box([=](not_null box) { + box->setTitle(tr::lng_url_auth_phone_sure_title()); + const auto confirm = [=](bool confirmed) { + return [=](Fn 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( + box->verticalLayout(), + bot, + st::defaultUserpicButton, + Ui::PeerUserpicShape::Forum) + : nullptr, + bot ? Info::Profile::NameValue(bot) : nullptr, + browser, + device, + ip, + region); + + 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; + })); } + +} // namespace UrlAuthBox diff --git a/Telegram/SourceFiles/boxes/url_auth_box.h b/Telegram/SourceFiles/boxes/url_auth_box.h index f48af20826..09416d0908 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.h +++ b/Telegram/SourceFiles/boxes/url_auth_box.h @@ -7,8 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "ui/layers/box_content.h" - class HistoryItem; struct HistoryMessageMarkupButton; @@ -16,56 +14,22 @@ namespace Main { class Session; } // namespace Main -class UrlAuthBox : public Ui::BoxContent { -public: - static void Activate( - not_null message, - int row, - int column); - static void Activate( - not_null session, - const QString &url, - QVariant context); +namespace Ui { +class GenericBox; +class Show; +} // namespace Ui -protected: - void prepare() override; +namespace UrlAuthBox { -private: - static void Request( - const MTPDurlAuthResultRequest &request, - not_null message, - int row, - int column); - static void Request( - const MTPDurlAuthResultRequest &request, - not_null session, - const QString &url, - QVariant context); +void ActivateButton( + std::shared_ptr show, + not_null message, + int row, + int column); +void ActivateUrl( + std::shared_ptr show, + not_null session, + const QString &url, + QVariant context); - enum class Result { - None, - Auth, - AuthAndAllowWrite, - }; - -public: - UrlAuthBox( - QWidget*, - not_null session, - const QString &url, - const QString &domain, - UserData *bot, - Fn callback); - -private: - not_null setupContent( - not_null session, - const QString &url, - const QString &domain, - UserData *bot, - Fn callback); - - Fn _callback; - not_null _content; - -}; +} // namespace UrlAuthBox diff --git a/Telegram/SourceFiles/boxes/url_auth_box_content.cpp b/Telegram/SourceFiles/boxes/url_auth_box_content.cpp new file mode 100644 index 0000000000..94240b3891 --- /dev/null +++ b/Telegram/SourceFiles/boxes/url_auth_box_content.cpp @@ -0,0 +1,354 @@ +/* +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/url_auth_box_content.h" + +#include "base/qthelp_url.h" +#include "lang/lang_keys.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/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/ui_utility.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +namespace UrlAuthBox { +namespace { + +} // namespace + +SwitchableUserpicButton::SwitchableUserpicButton( + not_null parent, + int size) +: RippleButton(parent, st::defaultRippleAnimation) +, _size(size) +, _userpicSize(st::restoreUserpicIcon.photoSize) +, _skip((_size - _userpicSize) / 2) { + resize(_size, _size); +} + +void SwitchableUserpicButton::setUserpic(not_null userpic) { + _userpic = userpic; + _userpic->setParent(this); + _userpic->moveToRight(_skip, _skip); + _userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + _userpic->show(); + update(); +} + +void SwitchableUserpicButton::setExpanded(bool expanded) { + if (_expanded == expanded) { + return; + } + _expanded = expanded; + const auto w = _expanded + ? (_size * 2.5 - _userpicSize) + : _size; + resize(w, _size); + if (_userpic) { + _userpic->moveToRight(_skip, _skip); + } + update(); +} + +void SwitchableUserpicButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + paintRipple(p, 0, 0); + + if (!_expanded) { + return; + } + + const auto arrowSize = st::lineWidth * 10; + const auto center = QPoint(_size / 2, height() / 2 + st::lineWidth * 3); + + auto pen = QPen(st::windowSubTextFg); + pen.setWidthF(st::lineWidth * 1.5); + p.setPen(pen); + p.setRenderHint(QPainter::Antialiasing); + + p.drawLine(center, center + QPoint(-arrowSize / 2, -arrowSize / 2)); + p.drawLine(center, center + QPoint(arrowSize / 2, -arrowSize / 2)); +} + +QImage SwitchableUserpicButton::prepareRippleMask() const { + return _expanded + ? Ui::RippleAnimation::RoundRectMask(size(), height() / 2) + : Ui::RippleAnimation::EllipseMask(size()); +} + +QPoint SwitchableUserpicButton::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()); +} + +void AddAuthInfoRow( + not_null container, + const QString &topText, + const QString &bottomText, + const QString &leftText, + const style::icon &icon) { + const auto row = container->add( + object_ptr(container), + st::boxRowPadding); + + const auto topLabel = Ui::CreateChild( + row, + topText, + st::urlAuthBoxRowTopLabel); + topLabel->setSelectable(true); + Ui::InstallTooltip(topLabel, [=] { + return (topLabel->textMaxWidth() > topLabel->width()) + ? topText + : QString(); + }); + const auto bottomLabel = Ui::CreateChild( + row, + bottomText, + st::urlAuthBoxRowBottomLabel); + bottomLabel->setSelectable(true); + Ui::InstallTooltip(bottomLabel, [=] { + return (bottomLabel->textMaxWidth() > bottomLabel->width()) + ? bottomText + : QString(); + }); + const auto leftLabel = Ui::CreateChild( + row, + leftText, + st::boxLabel); + + rpl::combine( + row->widthValue(), + topLabel->sizeValue(), + bottomLabel->sizeValue() + ) | rpl::on_next([=](int rowWidth, QSize topSize, QSize bottomSize) { + const auto totalHeight = topSize.height() + bottomSize.height(); + row->resize(rowWidth, totalHeight); + + const auto left = st::sessionValuePadding.left(); + const auto availableWidth = rowWidth + - leftLabel->width() + - left + - st::defaultVerticalListSkip; + + topLabel->resizeToNaturalWidth(availableWidth); + topLabel->moveToRight(0, 0); + bottomLabel->resizeToNaturalWidth(availableWidth); + bottomLabel->moveToRight(0, topSize.height()); + + leftLabel->moveToLeft(left, (totalHeight - leftLabel->height()) / 2); + }, row->lifetime()); + + { + const auto widget = Ui::CreateChild(row); + widget->resize(icon.size()); + + rpl::combine( + row->widthValue(), + topLabel->sizeValue(), + bottomLabel->sizeValue() + ) | rpl::on_next([=](int rowWidth, QSize topSize, QSize bottomSize) { + const auto totalHeight = topSize.height() + bottomSize.height(); + widget->moveToLeft(0, (totalHeight - leftLabel->height()) / 2); + }, row->lifetime()); + + widget->paintRequest() | rpl::on_next([=, &icon] { + auto p = QPainter(widget); + icon.paintInCenter(p, widget->rect()); + }, widget->lifetime()); + } +} + +void Show( + not_null box, + const QString &url, + const QString &domain, + const QString &selfName, + const QString &botName, + Fn callback) { + box->setWidth(st::boxWidth); + + box->addRow( + object_ptr( + box, + tr::lng_url_auth_open_confirm(tr::now, lt_link, url), + st::boxLabel), + st::boxPadding); + + const auto addCheckbox = [&](const TextWithEntities &text) { + const auto checkbox = box->addRow( + object_ptr( + box, + text, + true, + st::urlAuthCheckbox), + style::margins( + st::boxPadding.left(), + st::boxPadding.bottom(), + st::boxPadding.right(), + st::boxPadding.bottom())); + checkbox->setAllowTextLines(); + return checkbox; + }; + + const auto auth = addCheckbox( + tr::lng_url_auth_login_option( + tr::now, + lt_domain, + tr::bold(domain), + lt_user, + tr::bold(selfName), + tr::marked)); + + const auto allow = !botName.isEmpty() + ? addCheckbox(tr::lng_url_auth_allow_messages( + tr::now, + lt_bot, + tr::bold(botName), + tr::marked)) + : nullptr; + + if (allow) { + rpl::single( + auth->checked() + ) | rpl::then( + auth->checkedChanges() + ) | rpl::on_next([=](bool checked) { + if (!checked) { + allow->setChecked(false); + } + allow->setDisabled(!checked); + }, auth->lifetime()); + } + + box->addButton(tr::lng_open_link(), [=] { + const auto authed = auth->checked(); + const auto allowed = (authed && allow && allow->checked()); + callback({ + .auth = authed, + .allowWrite = allowed, + }); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +void ShowDetails( + not_null box, + const QString &url, + const QString &domain, + Fn callback, + object_ptr userpicOwned, + rpl::producer botName, + const QString &browser, + const QString &platform, + const QString &ip, + const QString ®ion) { + box->setWidth(st::boxWidth); + + const auto content = box->verticalLayout(); + + Ui::AddSkip(content); + Ui::AddSkip(content); + if (userpicOwned) { + const auto userpic = content->add( + std::move(userpicOwned), + st::boxRowPadding, + style::al_top); + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + Ui::AddSkip(content); + Ui::AddSkip(content); + } + + const auto domainUrl = qthelp::validate_url(domain); + const auto userpicButtonWidth = st::restoreUserpicIcon.photoSize; + const auto titlePadding = style::margins( + st::boxRowPadding.left(), + st::boxRowPadding.top(), + st::boxRowPadding.right() + userpicButtonWidth, + st::boxRowPadding.bottom()); + content->add( + object_ptr( + content, + domainUrl.isEmpty() + ? tr::lng_url_auth_login_button(tr::marked) + : tr::lng_url_auth_login_title( + lt_domain, + rpl::single(Ui::Text::Link(domain, domainUrl)), + tr::marked), + st::boxTitle), + titlePadding, + style::al_top); + Ui::AddSkip(content); + + content->add( + object_ptr( + content, + tr::lng_url_auth_site_access(tr::rich), + st::urlAuthCheckboxAbout), + st::boxRowPadding); + + Ui::AddSkip(content); + Ui::AddSkip(content); + if (!platform.isEmpty() || !browser.isEmpty()) { + AddAuthInfoRow( + content, + platform, + browser, + tr::lng_url_auth_device_label(tr::now), + st::menuIconDevices); + } + Ui::AddSkip(content); + Ui::AddSkip(content); + + if (!ip.isEmpty() || !region.isEmpty()) { + AddAuthInfoRow( + content, + ip, + region, + tr::lng_url_auth_ip_label(tr::now), + st::menuIconAddress); + } + Ui::AddSkip(content); + Ui::AddSkip(content); + + Ui::AddDividerText( + content, + rpl::single(tr::lng_url_auth_login_attempt(tr::now))); + Ui::AddSkip(content); + + auto allowMessages = (Ui::SettingsButton*)(nullptr); + if (botName) { + allowMessages = content->add( + object_ptr( + content, + tr::lng_url_auth_allow_messages_label())); + allowMessages->toggleOn(rpl::single(false)); + Ui::AddSkip(content); + Ui::AddDividerText( + content, + tr::lng_url_auth_allow_messages_about( + lt_bot, + std::move(botName))); + Ui::AddSkip(content); + } + + box->addButton(tr::lng_url_auth_login_button(), [=] { + callback({ + .auth = true, + .allowWrite = (allowMessages && allowMessages->toggled()), + }); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +} // namespace UrlAuthBox diff --git a/Telegram/SourceFiles/boxes/url_auth_box_content.h b/Telegram/SourceFiles/boxes/url_auth_box_content.h new file mode 100644 index 0000000000..ccaa0b480a --- /dev/null +++ b/Telegram/SourceFiles/boxes/url_auth_box_content.h @@ -0,0 +1,74 @@ +/* +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/widgets/buttons.h" + +namespace Ui { +class GenericBox; +class VerticalLayout; +} // namespace Ui + +namespace UrlAuthBox { + +struct Result { + bool auth : 1 = false; + bool allowWrite : 1 = false; + bool sharePhone : 1 = false; +}; + +class SwitchableUserpicButton final : public Ui::RippleButton { +public: + SwitchableUserpicButton( + not_null parent, + int size); + + void setExpanded(bool expanded); + void setUserpic(not_null); + +private: + void paintEvent(QPaintEvent *e) override; + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + + const int _size; + const int _userpicSize; + const int _skip; + bool _expanded = false; + Ui::RpWidget *_userpic = nullptr; + +}; + +void AddAuthInfoRow( + not_null container, + const QString &topText, + const QString &bottomText, + const QString &leftText, + const style::icon &icon); + +void Show( + not_null box, + const QString &url, + const QString &domain, + const QString &selfName, + const QString &botName, + Fn callback); + +void ShowDetails( + not_null box, + const QString &url, + const QString &domain, + Fn callback, + object_ptr userpicOwned, + rpl::producer botName, + const QString &browser, + const QString &platform, + const QString &ip, + const QString ®ion); + +} // namespace UrlAuthBox diff --git a/Telegram/SourceFiles/boxes/username_box.cpp b/Telegram/SourceFiles/boxes/username_box.cpp index 1134ff9905..03d70f0ef6 100644 --- a/Telegram/SourceFiles/boxes/username_box.cpp +++ b/Telegram/SourceFiles/boxes/username_box.cpp @@ -312,9 +312,10 @@ QString UsernameEditor::getName() const { } // namespace -void UsernamesBox( +void FillUsernamesBox( not_null box, - not_null peer) { + not_null peer, + Fn onSaved) { const auto isBot = peer && peer->isUser() && peer->asUser()->isBot(); box->setTitle(isBot ? tr::lng_bot_username_title() @@ -373,6 +374,9 @@ void UsernamesBox( ) | rpl::on_done([=] { editor->save( ) | rpl::on_done([=] { + if (onSaved) { + onSaved(); + } box->closeBox(); }, box->lifetime()); }, box->lifetime()); @@ -388,6 +392,19 @@ void UsernamesBox( } } +void UsernamesBox( + not_null box, + not_null peer) { + FillUsernamesBox(box, peer, nullptr); +} + +void UsernamesBoxWithCallback( + not_null box, + not_null peer, + Fn onSaved) { + FillUsernamesBox(box, peer, std::move(onSaved)); +} + void AddUsernameCheckLabel( not_null container, rpl::producer checkInfo) { diff --git a/Telegram/SourceFiles/boxes/username_box.h b/Telegram/SourceFiles/boxes/username_box.h index 9a845279f3..cf1b060b0b 100644 --- a/Telegram/SourceFiles/boxes/username_box.h +++ b/Telegram/SourceFiles/boxes/username_box.h @@ -18,6 +18,11 @@ void UsernamesBox( not_null box, not_null peer); +void UsernamesBoxWithCallback( + not_null box, + not_null peer, + Fn onSaved); + struct UsernameCheckInfo final { [[nodiscard]] static UsernameCheckInfo PurchaseAvailable( const QString &username, diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index db9a550624..f9b0d768cc 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -42,7 +42,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "apiwrap.h" #include "info/profile/info_profile_icon.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" +#include "settings/settings_common.h" #include "styles/style_info.h" // infoTopBarMenu #include "styles/style_layers.h" // st::boxLabel. #include "styles/style_calls.h" @@ -612,7 +613,7 @@ void BoxController::rowRightActionClicked(not_null row) { auto user = row->peer()->asUser(); Assert(user != nullptr); - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, {}); } void BoxController::receivedCalls(const QVector &result) { @@ -803,7 +804,9 @@ void ClearCallsBox( return result; } -void ShowCallsBox(not_null<::Window::SessionController*> window) { +void ShowCallsBox( + not_null<::Window::SessionController*> window, + bool highlightStartCall) { struct State { State(not_null<::Window::SessionController*> window) : callsController(window) @@ -872,7 +875,7 @@ void ShowCallsBox(not_null<::Window::SessionController*> window) { st::popupMenuWithIcons); const auto showSettings = [=] { window->showSettings( - Settings::Calls::Id(), + Settings::CallsId(), ::Window::SectionShow(anim::type::instant)); }; const auto clearAll = crl::guard(box, [=] { @@ -893,6 +896,13 @@ void ShowCallsBox(not_null<::Window::SessionController*> window) { state->menu->popup(QCursor::pos()); return true; }); + + if (highlightStartCall) { + box->showFinishes( + ) | rpl::take(1) | rpl::on_next([=] { + Settings::HighlightWidget(button); + }, box->lifetime()); + } })); } diff --git a/Telegram/SourceFiles/calls/calls_box_controller.h b/Telegram/SourceFiles/calls/calls_box_controller.h index 9c183f1088..d9ef0ccccb 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.h +++ b/Telegram/SourceFiles/calls/calls_box_controller.h @@ -81,6 +81,8 @@ void ClearCallsBox( not_null box, not_null<::Window::SessionController*> window); -void ShowCallsBox(not_null<::Window::SessionController*> window); +void ShowCallsBox( + not_null<::Window::SessionController*> window, + bool highlightStartCall = false); } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 6af96a3012..42c7806086 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -196,7 +196,9 @@ Instance::~Instance() { } } -void Instance::startOutgoingCall(not_null user, bool video) { +void Instance::startOutgoingCall( + not_null user, + StartOutgoingCallArgs args) { if (activateCurrentCall()) { return; } @@ -210,8 +212,8 @@ void Instance::startOutgoingCall(not_null user, bool video) { return; } requestPermissionsOrFail(crl::guard(this, [=] { - createCall(user, Call::Type::Outgoing, video); - }), video); + createCall(user, Call::Type::Outgoing, args); + }), args.video); } void Instance::startOrJoinGroupCall( @@ -413,7 +415,7 @@ void Instance::destroyCall(not_null call) { void Instance::createCall( not_null user, CallType type, - bool isVideo) { + StartOutgoingCallArgs args) { struct Performer final { explicit Performer(Fn callback) : callback(std::move(callback)) { @@ -455,7 +457,7 @@ void Instance::createCall( } _currentCallChanges.fire_copy(raw); }); - performer.callback(isVideo, false, performer); + performer.callback(args.video, args.isConfirmed, performer); } void Instance::destroyGroupCall(not_null call) { @@ -702,7 +704,7 @@ void Instance::handleCallUpdate( < base::unixtime::now()) { LOG(("Ignoring too old call.")); } else { - createCall(user, Call::Type::Incoming, phoneCall.is_video()); + createCall(user, Call::Type::Incoming, { phoneCall.is_video() }); _currentCall->handleUpdate(call); } } else if (!_currentCall diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 651cdded99..4742827c69 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -55,6 +55,11 @@ struct DhConfig; struct InviteRequest; struct StartConferenceInfo; +struct StartOutgoingCallArgs { + bool video = false; + bool isConfirmed = false; +}; + struct StartGroupCallArgs { enum class JoinConfirm { None, @@ -80,7 +85,7 @@ public: Instance(); ~Instance(); - void startOutgoingCall(not_null user, bool video); + void startOutgoingCall(not_null user, StartOutgoingCallArgs); void startOrJoinGroupCall( std::shared_ptr show, not_null peer, @@ -159,7 +164,10 @@ private: not_null ensureSoundLoaded(const QString &key); void playSoundOnce(const QString &key); - void createCall(not_null user, CallType type, bool isVideo); + void createCall( + not_null user, + CallType type, + StartOutgoingCallArgs); void destroyCall(not_null call); void finishConferenceInvitations(const StartConferenceInfo &args); diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index b01c1f1d80..8f57effdfb 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rect.h" #include "ui/integration.h" #include "core/application.h" +#include "core/core_settings.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" #include "main/main_session.h" @@ -59,6 +60,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/power_save_blocker.h" #include "media/streaming/media_streaming_utility.h" #include "window/main_window.h" +#include "window/window_controller.h" #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" #include "styles/style_calls.h" @@ -169,6 +171,28 @@ bool Panel::isActive() const { return window()->isActiveWindow() && isVisible(); } +QRect Panel::panelGeometry() const { + const auto saved = Core::App().settings().callPanelPosition(); + const auto adjusted = Core::AdjustToScale(saved, u"Call"_q); + const auto center = Core::App().getPointForCallPanelCenter(); + const auto simple = QRect(0, 0, st::callWidth, st::callHeight); + const auto initial = simple.translated(center - simple.center()); + const auto initialPosition = Core::WindowPosition{ + .moncrc = 0, + .scale = cScale(), + .x = initial.x(), + .y = initial.y(), + .w = initial.width(), + .h = initial.height(), + }; + return ::Window::CountInitialGeometry( + window(), + adjusted, + initialPosition, + { st::callWidthMin, st::callHeightMin }, + u"Call"_q); +} + ConferencePanelMigration Panel::migrationInfo() const { return ConferencePanelMigration{ .window = _window }; } @@ -207,6 +231,42 @@ void Panel::replaceCall(not_null call) { updateControlsGeometry(); } +void Panel::savePanelGeometry() { + if (!window()->windowHandle()) { + return; + } + const auto state = window()->windowHandle()->windowState(); + if (state == Qt::WindowMinimized) { + return; + } + const auto &savedPosition = Core::App().settings().callPanelPosition(); + auto realPosition = savedPosition; + if (state == Qt::WindowMaximized) { + realPosition.maximized = 1; + realPosition.moncrc = 0; + } else { + auto r = window()->body()->mapToGlobal(window()->body()->rect()); + realPosition.x = r.x(); + realPosition.y = r.y(); + realPosition.w = r.width(); + realPosition.h = r.height(); + realPosition.scale = cScale(); + realPosition.maximized = 0; + realPosition.moncrc = 0; + } + realPosition = ::Window::PositionWithScreen( + realPosition, + window(), + { st::callWidthMin, st::callHeightMin }, + u"Call"_q); + if (realPosition.w >= st::callWidthMin + && realPosition.h >= st::callHeightMin + && realPosition != savedPosition) { + Core::App().settings().setCallPanelPosition(realPosition); + Core::App().saveSettingsDelayed(); + } +} + void Panel::initWindow() { window()->setAttribute(Qt::WA_OpaquePaintEvent); window()->setAttribute(Qt::WA_NoSystemBackground); @@ -946,12 +1006,15 @@ rpl::lifetime &Panel::lifetime() { } void Panel::initGeometry() { - const auto center = Core::App().getPointForCallPanelCenter(); - const auto initRect = QRect(0, 0, st::callWidth, st::callHeight); - window()->setGeometry(initRect.translated(center - initRect.center())); + window()->setGeometry(panelGeometry()); window()->setMinimumSize({ st::callWidthMin, st::callHeightMin }); window()->show(); updateControlsGeometry(); + + _geometryLifetime = window()->geometryValue( + ) | rpl::skip(1) | rpl::on_next([=](QRect r) { + savePanelGeometry(); + }); } void Panel::initMediaDeviceToggles() { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index 66716a76be..aaed848f7f 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -79,6 +79,7 @@ public: [[nodiscard]] not_null user() const; [[nodiscard]] bool isVisible() const; [[nodiscard]] bool isActive() const; + [[nodiscard]] QRect panelGeometry() const; [[nodiscard]] ConferencePanelMigration migrationInfo() const; @@ -86,6 +87,7 @@ public: void minimize(); void toggleFullScreen(); void replaceCall(not_null call); + void savePanelGeometry(); void closeBeforeDestroy(bool windowIsReused = false); QWidget *chooseSourceParent() override; @@ -218,6 +220,7 @@ private: std::unique_ptr _background; + rpl::lifetime _geometryLifetime; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index 4b1c8b2fdf..5a81a58a20 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -1191,7 +1191,7 @@ object_ptr PrepareCreateCallBox( const auto &invite = selected.front(); Core::App().calls().startOutgoingCall( invite.user, - invite.video); + { invite.video }); } finished(true); }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 3f809df881..02329beaeb 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -2740,7 +2740,7 @@ void Panel::refreshTitle() { if (_call->rtmp()) { _titleSeparator.create( widget(), - rpl::single(QString::fromUtf8("\xE2\x80\xA2")), + rpl::single(Ui::kQBullet), st::groupCallTitleLabel); _titleSeparator->show(); _titleSeparator->setAttribute(Qt::WA_TransparentForMouseEvents); diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 810b798e11..6459b163b7 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -44,7 +44,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_device_resolver.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" +#include "settings/settings_common.h" #include "settings/settings_credits_graphics.h" #include "main/main_session.h" #include "apiwrap.h" diff --git a/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp b/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp index 67900ea5aa..23f20a55fd 100644 --- a/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp +++ b/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp @@ -33,7 +33,6 @@ public: Ui::BubbleRounding outer, RectParts sides) const override; - void startPaint(QPainter &p, const Ui::ChatStyle *st) const override; const style::TextStyle &textStyle() const override; void repaint(not_null item) const override; @@ -42,8 +41,13 @@ protected: QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, Ui::BubbleRounding rounding, float64 howMuchOver) const override; + void paintButtonStart( + QPainter &p, + const Ui::ChatStyle *st, + HistoryMessageMarkupButton::Color color) const override; void paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, @@ -54,6 +58,7 @@ protected: QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, int outerWidth, Ui::BubbleRounding rounding) const override; int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; @@ -69,8 +74,12 @@ Style::Style( : ReplyKeyboard::Style(st), _parent(parent) { } -void Style::startPaint(QPainter &p, const Ui::ChatStyle *st) const { - p.setPen(st::botKbColor); +void Style::paintButtonStart( + QPainter &p, + const Ui::ChatStyle *st, + HistoryMessageMarkupButton::Color color) const { + using Color = HistoryMessageMarkupButton::Color; + p.setPen((color == Color::Normal) ? st::botKbColor : st::white); p.setFont(st::botKbStyle.font); } @@ -93,9 +102,23 @@ void Style::paintButtonBg( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, Ui::BubbleRounding rounding, float64 howMuchOver) const { - Ui::FillRoundRect(p, rect, st::botKbBg, Ui::BotKeyboardCorners); + using Color = HistoryMessageMarkupButton::Color; + if (color == Color::Normal) { + Ui::FillRoundRect(p, rect, st::botKbBg, Ui::BotKeyboardCorners); + } else { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush((color == Color::Primary) + ? QColor(0x29, 0x8a, 0xcf) + : (color == Color::Danger) + ? QColor(0xe0, 0x53, 0x56) + : QColor(0x61, 0xc7, 0x52)); + const auto radius = st::roundRadiusSmall; + p.drawRoundedRect(rect, radius, radius); + } } void Style::paintButtonIcon( @@ -111,6 +134,7 @@ void Style::paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, int outerWidth, Ui::BubbleRounding rounding) const { // Buttons with loading progress should not appear here. @@ -148,7 +172,8 @@ void BotKeyboard::paintEvent(QPaintEvent *e) { nullptr, Ui::BubbleRounding(), width(), - clip.translated(-x, -st::botKbScroll.deltat)); + clip.translated(-x, -st::botKbScroll.deltat), + _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any)); } } diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index cce2e5bf54..bdd5b0aa46 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -154,10 +154,7 @@ SendButton { sendIconFillPadding: pixels; inner: IconButton; stars: RoundButton; - record: icon; - recordOver: icon; - round: icon; - roundOver: icon; + recordSize: size; sendDisabledFg: color; } @@ -1289,18 +1286,11 @@ historyRecordVoiceFgOver: historyComposeIconFgOver; historyRecordVoiceFgInactive: attentionButtonFg; historyRecordVoiceFgActive: windowBgActive; historyRecordVoiceFgActiveIcon: windowFgActive; -historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; -historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }}; historyRecordVoiceOnceBgOver: icon {{ "voice_lock/audio_once_bg", historySendIconFgOver }}; historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }}; -historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; -historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }}; -historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }}; -historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }}; -historyRecordRoundIconPosition: point(0px, 0px); historyRecordSendIconPosition: point(2px, 0px); historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordSignalRadius: 5px; @@ -1413,12 +1403,10 @@ historySend: SendButton { textTop: 5px; width: -8px; } - record: historyRecordVoice; - recordOver: historyRecordVoiceOver; - round: historyRecordRound; - roundOver: historyRecordRoundOver; + recordSize: size(26px, 26px); sendDisabledFg: historyComposeIconFg; } +historyRecordFrameIndex: 30; defaultComposeFilesMenu: IconButton(defaultIconButton) { width: 48px; @@ -1748,3 +1736,19 @@ menuTranscribeDummyButton: IconButton(defaultIconButton) { } roundVideoFont: font(14px semibold); + +topPeersSelectorUserpicSize: 31px; +topPeersSelectorUserpicGap: 8px; +topPeersSelectorPadding: 6px; +topPeersSelectorUserpicExpand: 0.1; +topPeersSelectorSkip: point(9px, -5px); + +topPeersSelectorImportantTooltip: ImportantTooltip(defaultImportantTooltip) { + bg: msgServiceBg; + radius: 9px; +} +topPeersSelectorImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { + style: TextStyle(defaultTextStyle) { + font: font(semibold 12px); + } +} diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 2b82521fb7..9f5484cdc2 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -50,7 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "core/core_settings.h" #include "core/application.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "styles/style_chat_helpers.h" diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 13e3ff2cb4..4f30f5f7c2 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -1670,7 +1670,6 @@ void InitFieldAutocomplete( const auto field = descriptor.field; field->rawTextEdit()->installEventFilter(raw); - field->customTab(true); raw->mentionChosen( ) | rpl::on_next([=](FieldAutocomplete::MentionChosen data) { @@ -1735,9 +1734,10 @@ void InitFieldAutocomplete( } field->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { if (!raw->isHidden()) { raw->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab); + *handled = true; } }, raw->lifetime()); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index d7ec453aac..91b37bb1e7 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -45,7 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "main/main_session.h" #include "settings/settings_common.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" @@ -246,9 +246,6 @@ void EditLinkBox( } }); - url->customTab(true); - text->customTab(true); - const auto clearFullSelection = [=](not_null input) { if (input->empty()) { return; @@ -264,17 +261,20 @@ void EditLinkBox( }; url->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { clearFullSelection(url); text->setFocus(); + *handled = true; }, url->lifetime()); + text->tabbed( - ) | rpl::on_next([=] { + ) | rpl::on_next([=](not_null handled) { if (!url->empty()) { url->selectAll(); } clearFullSelection(text); url->setFocus(); + *handled = true; }, text->lifetime()); } diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 3822842c82..e16041a988 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -1117,6 +1117,21 @@ void Application::checkStartUrls() { } bool Application::openLocalUrl(const QString &url, QVariant context) { + const auto urlTrimmed = url.trimmed(); + const auto protocol = u"tg://"_q; + if (urlTrimmed.startsWith(protocol, Qt::CaseInsensitive) + && !passcodeLocked()) { + const auto command = urlTrimmed.mid(protocol.size()); + const auto my = context.value(); + const auto controller = my.sessionWindow.get() + ? my.sessionWindow.get() + : _lastActivePrimaryWindow + ? _lastActivePrimaryWindow->sessionController() + : nullptr; + if (TryRouterForLocalUrl(controller, command)) { + return true; + } + } return openCustomUrl("tg://", LocalUrlHandlers(), url, context); } diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index b0a9a66eb2..9b1f21fb3b 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -135,7 +135,7 @@ void Changelogs::addBetaLog(int changeVersion, const char *changes) { } const auto text = [&] { static const auto simple = u"\n- "_q; - static const auto separator = QString::fromUtf8("\n\xE2\x80\xA2 "); + static const auto separator = '\n' + Ui::kQBullet + ' '; auto result = QString::fromUtf8(changes).trimmed(); if (result.startsWith(base::StringViewMid(simple, 1))) { result = separator.mid(1) + result.mid(simple.size() - 1); diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 520b1282a1..a1cd3bd35b 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -18,6 +18,7 @@ constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kPhoneNumberLinkProperty = 0x07; constexpr auto kTodoListItemIdProperty = 0x08; +constexpr auto kFastShareProperty = 0x09; namespace Ui { class Show; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index ddb215d956..01ef35c0af 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -159,6 +159,8 @@ QByteArray Settings::serialize() const { LogPosition(_mediaViewPosition, u"Viewer"_q); const auto ivPosition = Serialize(_ivPosition); LogPosition(_ivPosition, u"IV"_q); + const auto callPanelPosition = Serialize(_callPanelPosition); + LogPosition(_callPanelPosition, u"CallPanel"_q); const auto proxy = _proxy.serialize(); const auto skipLanguages = _skipTranslationLanguages.current(); @@ -246,7 +248,8 @@ QByteArray Settings::serialize() const { + Serialize::bytearraySize(_tonsiteStorageToken) + sizeof(qint32) * 8 + sizeof(ushort) - + sizeof(qint32); // _notificationsDisplayChecksum + + sizeof(qint32) // _notificationsDisplayChecksum + + Serialize::bytearraySize(callPanelPosition); auto result = QByteArray(); result.reserve(size); @@ -410,7 +413,8 @@ QByteArray Settings::serialize() const { << qint32(_systemDarkModeEnabled.current() ? 1 : 0) << qint32(_quickDialogAction) << _notificationsVolume - << _notificationsDisplayChecksum; + << _notificationsDisplayChecksum + << callPanelPosition; } Ensures(result.size() == size); @@ -533,6 +537,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0); qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0; QByteArray ivPosition; + QByteArray callPanelPosition; QString customFontFamily = _customFontFamily; qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0; qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2; @@ -881,6 +886,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> notificationsDisplayChecksum; } + if (!stream.atEnd()) { + stream >> callPanelPosition; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -1093,6 +1101,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!ivPosition.isEmpty()) { _ivPosition = Deserialize(ivPosition); } + if (!callPanelPosition.isEmpty()) { + _callPanelPosition = Deserialize(callPanelPosition); + } _customFontFamily = customFontFamily; _systemUnlockEnabled = (systemUnlockEnabled == 1); _weatherInCelsius = !weatherInCelsius diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 2e3cce6aa1..b1200810ea 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -909,6 +909,13 @@ public: _ivPosition = position; } + [[nodiscard]] const WindowPosition &callPanelPosition() const { + return _callPanelPosition; + } + void setCallPanelPosition(const WindowPosition &position) { + _callPanelPosition = position; + } + [[nodiscard]] QString customFontFamily() const { return _customFontFamily; } @@ -1091,6 +1098,7 @@ private: rpl::variable _storiesClickTooltipHidden = false; rpl::variable _ttlVoiceClickTooltipHidden = false; WindowPosition _ivPosition; + WindowPosition _callPanelPosition; QString _customFontFamily; bool _systemUnlockEnabled = false; std::optional _weatherInCelsius; diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_chats.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_chats.cpp new file mode 100644 index 0000000000..ebc66af146 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_chats.cpp @@ -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 +*/ +#include "core/deep_links/deep_links_router.h" + +#include "dialogs/dialogs_key.h" +#include "mainwidget.h" +#include "mainwindow.h" +#include "window/window_session_controller.h" + +namespace Core::DeepLinks { +namespace { + +Result FocusSearch(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->content()->searchMessages(QString(), Dialogs::Key()); + return Result::Handled; +} + +Result ShowEmojiStatus(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(u"main-menu/emoji-status"_q); + ctx.controller->widget()->showMainMenu(); + return Result::Handled; +} + +} // namespace + +void RegisterChatsHandlers(Router &router) { + router.add(u"chats"_q, { + .path = u"search"_q, + .action = CodeBlock{ FocusSearch }, + .skipActivation = true, + }); + + router.add(u"chats"_q, { + .path = u"emoji-status"_q, + .action = CodeBlock{ ShowEmojiStatus }, + }); +} + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_chats.h b/Telegram/SourceFiles/core/deep_links/deep_links_chats.h new file mode 100644 index 0000000000..434812d290 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_chats.h @@ -0,0 +1,16 @@ +/* +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 Core::DeepLinks { + +class Router; + +void RegisterChatsHandlers(Router &router); + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_contacts.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_contacts.cpp new file mode 100644 index 0000000000..d7158a2969 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_contacts.cpp @@ -0,0 +1,67 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "core/deep_links/deep_links_router.h" + +#include "boxes/peer_list_controllers.h" +#include "window/window_session_controller.h" + +namespace Core::DeepLinks { +namespace { + +Result ShowContacts(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(PrepareContactsBox(ctx.controller)); + return Result::Handled; +} + +Result ShowAddContact(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showAddContact(); + return Result::Handled; +} + +} // namespace + +void RegisterContactsHandlers(Router &router) { + router.add(u"contacts"_q, { + .path = QString(), + .action = CodeBlock{ [](const Context &ctx) { + return ShowContacts(ctx); + }}, + }); + + router.add(u"contacts"_q, { + .path = u"search"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowContacts(ctx); + }}, + }); + + router.add(u"contacts"_q, { + .path = u"sort"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(u"contacts/sort"_q); + ctx.controller->show(PrepareContactsBox(ctx.controller)); + return Result::Handled; + }}, + }); + + router.add(u"contacts"_q, { + .path = u"new"_q, + .action = CodeBlock{ ShowAddContact }, + }); +} + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_contacts.h b/Telegram/SourceFiles/core/deep_links/deep_links_contacts.h new file mode 100644 index 0000000000..0a4c0d8300 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_contacts.h @@ -0,0 +1,16 @@ +/* +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 Core::DeepLinks { + +class Router; + +void RegisterContactsHandlers(Router &router); + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp new file mode 100644 index 0000000000..cbcd6037ff --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp @@ -0,0 +1,58 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "core/deep_links/deep_links_router.h" + +#include "window/window_session_controller.h" + +namespace Core::DeepLinks { +namespace { + +Result ShowNewGroup(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showNewGroup(); + return Result::Handled; +} + +Result ShowNewChannel(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showNewChannel(); + return Result::Handled; +} + +Result ShowAddContact(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showAddContact(); + return Result::Handled; +} + +} // namespace + +void RegisterNewHandlers(Router &router) { + router.add(u"new"_q, { + .path = u"group"_q, + .action = CodeBlock{ ShowNewGroup }, + }); + + router.add(u"new"_q, { + .path = u"channel"_q, + .action = CodeBlock{ ShowNewChannel }, + }); + + router.add(u"new"_q, { + .path = u"contact"_q, + .action = CodeBlock{ ShowAddContact }, + }); +} + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_new.h b/Telegram/SourceFiles/core/deep_links/deep_links_new.h new file mode 100644 index 0000000000..54d356e2ce --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_new.h @@ -0,0 +1,16 @@ +/* +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 Core::DeepLinks { + +class Router; + +void RegisterNewHandlers(Router &router); + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_router.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_router.cpp new file mode 100644 index 0000000000..cb045bc498 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_router.cpp @@ -0,0 +1,185 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "core/deep_links/deep_links_router.h" + +#include "core/deep_links/deep_links_chats.h" +#include "core/deep_links/deep_links_contacts.h" +#include "core/deep_links/deep_links_new.h" +#include "core/deep_links/deep_links_settings.h" +#include "core/application.h" +#include "main/main_session.h" +#include "ui/toast/toast.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" + +namespace Core::DeepLinks { +namespace { + +Context ParseCommand( + Window::SessionController *controller, + const QString &command) { + auto result = Context{ .controller = controller }; + + auto path = command; + auto queryStart = path.indexOf('?'); + if (queryStart >= 0) { + const auto query = path.mid(queryStart + 1); + path = path.left(queryStart); + for (const auto &pair : query.split('&')) { + const auto eq = pair.indexOf('='); + if (eq > 0) { + result.params[pair.left(eq).toLower()] = pair.mid(eq + 1); + } else if (!pair.isEmpty()) { + result.params[pair.toLower()] = QString(); + } + } + } + + path = path.trimmed(); + while (path.startsWith('/')) { + path = path.mid(1); + } + while (path.endsWith('/')) { + path.chop(1); + } + + const auto slash = path.indexOf('/'); + if (slash > 0) { + result.section = path.left(slash).toLower(); + result.path = path.mid(slash + 1); + } else { + result.section = path.toLower(); + } + + return result; +} + +} // namespace + +Router &Router::Instance() { + static auto instance = Router(); + return instance; +} + +Router::Router() { + RegisterSettingsHandlers(*this); + RegisterContactsHandlers(*this); + RegisterChatsHandlers(*this); + RegisterNewHandlers(*this); +} + +void Router::add(const QString §ion, Entry entry) { + _handlers[section].push_back(std::move(entry)); +} + +bool Router::tryHandle( + Window::SessionController *controller, + const QString &command) { + const auto ctx = ParseCommand(controller, command); + const auto [result, skipActivation] = dispatch(ctx); + + switch (result) { + case Result::Handled: + if (controller && !skipActivation) { + controller->window().activate(); + } + return true; + case Result::NeedsAuth: + return false; + case Result::Unsupported: + showUnsupportedMessage(controller, command); + return true; + case Result::NotFound: + return false; + } + return false; +} + +Router::DispatchResult Router::dispatch(const Context &ctx) { + if (ctx.section.isEmpty()) { + return { Result::NotFound }; + } + return handleSection(ctx.section, ctx); +} + +Router::DispatchResult Router::handleSection( + const QString §ion, + const Context &ctx) { + const auto it = _handlers.find(section); + if (it == _handlers.end()) { + return { Result::NotFound }; + } + + const auto &entries = it->second; + const auto path = ctx.path.toLower(); + + for (const auto &entry : entries) { + if (entry.path == path || (entry.path.isEmpty() && path.isEmpty())) { + if (entry.requiresAuth && !ctx.controller) { + return { Result::NeedsAuth }; + } + return { + executeAction(entry.action, ctx), + entry.skipActivation, + }; + } + } + + for (const auto &entry : entries) { + if (!entry.path.isEmpty() && path.startsWith(entry.path + '/')) { + if (entry.requiresAuth && !ctx.controller) { + return { Result::NeedsAuth }; + } + return { + executeAction(entry.action, ctx), + entry.skipActivation, + }; + } + } + + return { Result::Unsupported }; +} + +Result Router::executeAction(const Action &action, const Context &ctx) { + return v::match(action, [&](const SettingsSection &s) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showSettings(s.sectionId); + return Result::Handled; + }, [&](const SettingsControl &s) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + if (!s.controlId.isEmpty()) { + ctx.controller->setHighlightControlId(s.controlId); + } + ctx.controller->showSettings(s.sectionId); + return Result::Handled; + }, [&](const CodeBlock &c) { + return c.handler(ctx); + }, [&](const AliasTo &a) { + auto aliasCtx = ctx; + aliasCtx.section = a.section; + aliasCtx.path = a.path; + return handleSection(a.section, aliasCtx).result; + }); +} + +void Router::showUnsupportedMessage( + Window::SessionController *controller, + const QString &url) { + const auto text = u"This link is not supported on Desktop."_q; + if (controller) { + controller->showToast(text); + } else if (const auto window = Core::App().activeWindow()) { + window->showToast(text); + } +} + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_router.h b/Telegram/SourceFiles/core/deep_links/deep_links_router.h new file mode 100644 index 0000000000..bd519823b9 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_router.h @@ -0,0 +1,48 @@ +/* +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 "core/deep_links/deep_links_types.h" + +namespace Core::DeepLinks { + +class Router final { +public: + static Router &Instance(); + + [[nodiscard]] bool tryHandle( + Window::SessionController *controller, + const QString &command); + + void add(const QString §ion, Entry entry); + +private: + struct DispatchResult { + Result result = Result::NotFound; + bool skipActivation = false; + }; + + Router(); + + [[nodiscard]] DispatchResult dispatch(const Context &ctx); + [[nodiscard]] DispatchResult handleSection( + const QString §ion, + const Context &ctx); + [[nodiscard]] Result executeAction( + const Action &action, + const Context &ctx); + + void showUnsupportedMessage( + Window::SessionController *controller, + const QString &url); + + std::map> _handlers; + +}; + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp new file mode 100644 index 0000000000..2bf81c8c03 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp @@ -0,0 +1,2127 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "core/deep_links/deep_links_router.h" + +#include "apiwrap.h" +#include "base/binary_guard.h" +#include "boxes/add_contact_box.h" +#include "boxes/gift_credits_box.h" +#include "boxes/language_box.h" +#include "boxes/stickers_box.h" +#include "chat_helpers/emoji_sets_manager.h" +#include "boxes/edit_privacy_box.h" +#include "boxes/peers/edit_peer_color_box.h" +#include "info/bot/earn/info_bot_earn_widget.h" +#include "info/bot/starref/info_bot_starref_common.h" +#include "info/bot/starref/info_bot_starref_join_widget.h" +#include "settings/settings_privacy_controllers.h" +#include "ui/chat/chat_style.h" +#include "boxes/star_gift_box.h" +#include "ui/boxes/confirm_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "boxes/username_box.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "data/data_user.h" +#include "data/notify/data_notify_settings.h" +#include "info/info_memento.h" +#include "info/peer_gifts/info_peer_gifts_widget.h" +#include "info/settings/info_settings_widget.h" +#include "info/stories/info_stories_widget.h" +#include "lang/lang_keys.h" +#include "ui/boxes/peer_qr_box.h" +#include "ui/layers/generic_box.h" +#include "main/main_domain.h" +#include "main/main_session.h" +#include "storage/storage_domain.h" +#include "settings/sections/settings_active_sessions.h" +#include "settings/sections/settings_advanced.h" +#include "settings/sections/settings_blocked_peers.h" +#include "settings/sections/settings_business.h" +#include "settings/sections/settings_calls.h" +#include "settings/sections/settings_chat.h" +#include "settings/sections/settings_passkeys.h" +#include "data/components/passkeys.h" +#include "calls/calls_box_controller.h" +#include "settings/sections/settings_credits.h" +#include "settings/sections/settings_folders.h" +#include "settings/sections/settings_global_ttl.h" +#include "settings/sections/settings_information.h" +#include "settings/sections/settings_local_passcode.h" +#include "settings/sections/settings_main.h" +#include "settings/cloud_password/settings_cloud_password_email_confirm.h" +#include "settings/cloud_password/settings_cloud_password_input.h" +#include "settings/cloud_password/settings_cloud_password_start.h" +#include "settings/cloud_password/settings_cloud_password_login_email.h" +#include "api/api_cloud_password.h" +#include "core/core_cloud_password.h" +#include "settings/sections/settings_notifications.h" +#include "settings/sections/settings_notifications_type.h" +#include "settings/settings_power_saving.h" +#include "settings/settings_search.h" +#include "settings/sections/settings_premium.h" +#include "ui/power_saving.h" +#include "settings/sections/settings_privacy_security.h" +#include "settings/sections/settings_websites.h" +#include "boxes/connection_box.h" +#include "boxes/local_storage_box.h" +#include "mainwindow.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" + +namespace Core::DeepLinks { +namespace { + +Result ShowLanguageBox(const Context &ctx, const QString &highlightId = QString()) { + static auto Guard = base::binary_guard(); + if (!highlightId.isEmpty() && ctx.controller) { + ctx.controller->setHighlightControlId(highlightId); + } + Guard = LanguageBox::Show(ctx.controller, highlightId); + return Result::Handled; +} + +Result ShowPowerSavingBox( + const Context &ctx, + PowerSaving::Flags highlightFlags = PowerSaving::Flags()) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show( + Box(::Settings::PowerSavingBox, highlightFlags), + Ui::LayerOption::KeepOther, + anim::type::normal); + return Result::Handled; +} + +Result ShowMainMenuWithHighlight(const Context &ctx, const QString &highlightId) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(highlightId); + ctx.controller->widget()->showMainMenu(); + return Result::Handled; +} + +Result ShowSavedMessages(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showPeerHistory( + ctx.controller->session().userPeerId(), + Window::SectionShow::Way::Forward); + return Result::Handled; +} + +Result ShowFaq(const Context &ctx) { + ::Settings::OpenFaq( + ctx.controller ? base::make_weak(ctx.controller) : nullptr); + return Result::Handled; +} + +void ShowQrBox(not_null controller) { + const auto user = controller->session().user(); + controller->uiShow()->show(Box( + Ui::FillPeerQrBox, + user.get(), + std::nullopt, + rpl::single(QString()))); +} + +Result ShowPeerColorBox( + const Context &ctx, + PeerColorTab tab, + const QString &highlightId = QString()) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + if (!highlightId.isEmpty()) { + ctx.controller->setHighlightControlId(highlightId); + } + ctx.controller->show(Box( + EditPeerColorBox, + ctx.controller, + ctx.controller->session().user(), + std::shared_ptr(), + std::shared_ptr(), + tab)); + return Result::Handled; +} + +Result HandleQrCode(const Context &ctx, bool highlightCopy) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + + if (highlightCopy) { + ctx.controller->setHighlightControlId(u"self-qr-code/copy"_q); + } + + const auto user = ctx.controller->session().user(); + if (!user->username().isEmpty()) { + ShowQrBox(ctx.controller); + } else { + const auto controller = ctx.controller; + controller->uiShow()->show(Box( + UsernamesBoxWithCallback, + user, + [=] { ShowQrBox(controller); })); + } + return Result::Handled; +} + +Result ShowEditName( + const Context &ctx, + EditNameBox::Focus focus = EditNameBox::Focus::FirstName) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + if (ctx.controller->showFrozenError()) { + return Result::Handled; + } + ctx.controller->show(Box( + ctx.controller->session().user(), + focus)); + return Result::Handled; +} + +Result ShowEditUsername(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + if (ctx.controller->showFrozenError()) { + return Result::Handled; + } + ctx.controller->show(Box(UsernamesBox, ctx.controller->session().user())); + return Result::Handled; +} + +Result OpenInternalUrl(const Context &ctx, const QString &url) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + Core::App().openInternalUrl( + url, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = base::make_weak(ctx.controller), + })); + return Result::Handled; +} + +Result ShowMyProfile(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showSection( + Info::Stories::Make(ctx.controller->session().user())); + return Result::Handled; +} + +Result ShowLogOutMenu(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(u"settings/log-out"_q); + ctx.controller->showSettings(::Settings::MainId()); + return Result::Handled; +} + +Result ShowPasskeys(const Context &ctx, bool highlightCreate) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto controller = ctx.controller; + const auto session = &controller->session(); + const auto showBox = [=] { + if (highlightCreate) { + controller->setHighlightControlId(u"passkeys/create"_q); + } + if (session->passkeys().list().empty()) { + controller->show(Box([=](not_null box) { + ::Settings::PasskeysNoneBox(box, session); + box->boxClosing() | rpl::on_next([=] { + if (!session->passkeys().list().empty()) { + controller->showSettings(::Settings::PasskeysId()); + } + }, box->lifetime()); + })); + } else { + controller->showSettings(::Settings::PasskeysId()); + } + }; + if (session->passkeys().listKnown()) { + showBox(); + } else { + session->passkeys().requestList( + ) | rpl::take(1) | rpl::on_next([=] { + showBox(); + }, controller->lifetime()); + } + return Result::Handled; +} + +Result ShowAutoDeleteSetCustom(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(u"auto-delete/set-custom"_q); + ctx.controller->showSettings(::Settings::GlobalTTLId()); + return Result::Handled; +} + +Result ShowLoginEmail(const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto controller = ctx.controller; + controller->session().api().cloudPassword().reload(); + controller->uiShow()->show(Box([=](not_null box) { + { + box->getDelegate()->setTitle( + controller->session().api().cloudPassword().state( + ) | rpl::map([](const Core::CloudPasswordState &state) { + return state.loginEmailPattern; + }) | rpl::map([](QString email) { + if (email.contains(' ')) { + return tr::lng_settings_cloud_login_email_section_title( + tr::now, + tr::rich); + } + return Ui::Text::WrapEmailPattern(std::move(email)); + })); + for (const auto &child : ranges::views::reverse( + box->parentWidget()->children())) { + if (child && child->isWidgetType()) { + (static_cast(child))->setAttribute( + Qt::WA_TransparentForMouseEvents); + break; + } + } + } + Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ + .text = tr::lng_settings_cloud_login_email_box_about(), + .confirmed = [=](Fn close) { + controller->showSettings(::Settings::CloudLoginEmailId()); + controller->window().activate(); + close(); + }, + .confirmText = tr::lng_settings_cloud_login_email_box_ok(), + }); + })); + return Result::Handled; +} + +Result ShowNotificationType( + const Context &ctx, + Data::DefaultNotify type, + const QString &highlightId = QString()) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + if (!highlightId.isEmpty()) { + ctx.controller->setHighlightControlId(highlightId); + } + ctx.controller->showSettings(::Settings::NotificationsType::Id(type)); + return Result::Handled; +} + +using PrivacyKey = Api::UserPrivacy::Key; + +template +Result ShowPrivacyBox( + const Context &ctx, + PrivacyKey key, + ControllerFactory controllerFactory, + const QString &highlightControl = QString()) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto controller = ctx.controller; + const auto session = &controller->session(); + if (!highlightControl.isEmpty()) { + controller->setHighlightControlId(highlightControl); + } + const auto shower = std::make_shared(); + *shower = session->api().userPrivacy().value( + key + ) | rpl::take( + 1 + ) | rpl::on_next(crl::guard(controller, [=, shower = shower]( + const Api::UserPrivacy::Rule &value) { + controller->show(Box( + controller, + controllerFactory(), + value)); + })); + session->api().userPrivacy().reload(key); + return Result::Handled; +} + +} // namespace + +void RegisterSettingsHandlers(Router &router) { + router.add(u"settings"_q, { + .path = QString(), + .action = SettingsSection{ ::Settings::MainId() }, + }); + + router.add(u"settings"_q, { + .path = u"edit"_q, + .action = SettingsSection{ ::Settings::InformationId() }, + }); + + router.add(u"settings"_q, { + .path = u"my-profile"_q, + .action = CodeBlock{ ShowMyProfile }, + }); + + router.add(u"settings"_q, { + .path = u"my-profile/edit"_q, + .action = SettingsSection{ ::Settings::InformationId() }, + }); + + router.add(u"settings"_q, { + .path = u"my-profile/posts"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(u"my-profile/posts"_q); + return ShowMyProfile(ctx); + }}, + }); + + router.add(u"settings"_q, { + .path = u"my-profile/posts/add-album"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->setHighlightControlId(u"my-profile/posts/add-album"_q); + return ShowMyProfile(ctx); + }}, + }); + + router.add(u"settings"_q, { + .path = u"my-profile/gifts"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showSection( + Info::PeerGifts::Make(ctx.controller->session().user())); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"my-profile/archived-posts"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showSection(Info::Stories::Make( + ctx.controller->session().user(), + Info::Stories::ArchiveId())); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"emoji-status"_q, + .action = AliasTo{ u"chats"_q, u"emoji-status"_q }, + }); + + router.add(u"settings"_q, { + .path = u"profile-color"_q, + .action = AliasTo{ u"settings"_q, u"edit/your-color"_q }, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/profile"_q, + .action = AliasTo{ u"settings"_q, u"edit/your-color"_q }, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/profile/add-icons"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox( + ctx, + PeerColorTab::Profile, + u"profile-color/add-icons"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/profile/use-gift"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox( + ctx, + PeerColorTab::Profile, + u"profile-color/use-gift"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/profile/reset"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox( + ctx, + PeerColorTab::Profile, + u"profile-color/reset"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/name"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox(ctx, PeerColorTab::Name); + }}, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/name/add-icons"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox( + ctx, + PeerColorTab::Name, + u"profile-color/add-icons"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"profile-color/name/use-gift"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox( + ctx, + PeerColorTab::Name, + u"profile-color/use-gift"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"profile-photo"_q, + .action = AliasTo{ u"settings"_q, u"edit/set-photo"_q }, + }); + + router.add(u"settings"_q, { + .path = u"profile-photo/use-emoji"_q, + .action = SettingsControl{ + ::Settings::MainId(), + u"profile-photo/use-emoji"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"devices"_q, + .action = SettingsSection{ ::Settings::SessionsId() }, + }); + + router.add(u"settings"_q, { + .path = u"folders"_q, + .action = SettingsSection{ ::Settings::FoldersId() }, + }); + + router.add(u"settings"_q, { + .path = u"notifications"_q, + .action = SettingsSection{ ::Settings::NotificationsId() }, + }); + + router.add(u"settings"_q, { + .path = u"privacy"_q, + .action = SettingsSection{ ::Settings::PrivacySecurityId() }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/blocked"_q, + .action = SettingsSection{ ::Settings::BlockedPeersId() }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/blocked/block-user"_q, + .action = SettingsControl{ + ::Settings::BlockedPeersId(), + u"blocked/block-user"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/active-websites"_q, + .action = SettingsSection{ ::Settings::WebsitesId() }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/active-websites/disconnect-all"_q, + .action = SettingsControl{ + ::Settings::WebsitesId(), + u"websites/disconnect-all"_q, + }, + }); + + const auto openPasscode = [](const Context &ctx, const QString &highlight) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + if (!highlight.isEmpty()) { + ctx.controller->setHighlightControlId(highlight); + } + const auto &local = ctx.controller->session().domain().local(); + if (local.hasLocalPasscode()) { + ctx.controller->showSettings(::Settings::LocalPasscodeCheckId()); + } else { + ctx.controller->showSettings(::Settings::LocalPasscodeCreateId()); + } + return Result::Handled; + }; + router.add(u"settings"_q, { + .path = u"privacy/passcode"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openPasscode(ctx, QString()); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/passcode/disable"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openPasscode(ctx, u"passcode/disable"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/passcode/change"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openPasscode(ctx, u"passcode/change"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/passcode/auto-lock"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openPasscode(ctx, u"passcode/auto-lock"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/passcode/face-id"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openPasscode(ctx, u"passcode/biometrics"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/passcode/fingerprint"_q, + .action = AliasTo{ u"settings"_q, u"privacy/passcode/face-id"_q }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/auto-delete"_q, + .action = SettingsSection{ ::Settings::GlobalTTLId() }, + }); + + const auto openCloudPassword = [](const Context &ctx, const QString &highlight) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->showCloudPassword(highlight); + return Result::Handled; + }; + router.add(u"settings"_q, { + .path = u"privacy/2sv"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openCloudPassword(ctx, QString()); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/2sv/change"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openCloudPassword(ctx, u"2sv/change"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/2sv/disable"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openCloudPassword(ctx, u"2sv/disable"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"privacy/2sv/change-email"_q, + .action = CodeBlock{ [=](const Context &ctx) { + return openCloudPassword(ctx, u"2sv/change-email"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/passkey"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPasskeys(ctx, false); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/passkey/create"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPasskeys(ctx, true); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/auto-delete/set-custom"_q, + .action = CodeBlock{ ShowAutoDeleteSetCustom }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/phone-number"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::PhoneNumber, + [=] { return std::make_unique<::Settings::PhoneNumberPrivacyController>(ctx.controller); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/phone-number/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::PhoneNumber, + [=] { return std::make_unique<::Settings::PhoneNumberPrivacyController>(ctx.controller); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/phone-number/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::PhoneNumber, + [=] { return std::make_unique<::Settings::PhoneNumberPrivacyController>(ctx.controller); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/last-seen"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::LastSeen, + [=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/last-seen/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::LastSeen, + [=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/last-seen/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::LastSeen, + [=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/last-seen/hide-read-time"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::LastSeen, + [=] { return std::make_unique<::Settings::LastSeenPrivacyController>(&ctx.controller->session()); }, + u"privacy/hide-read-time"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/profile-photos"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::ProfilePhoto, + [=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/profile-photos/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::ProfilePhoto, + [=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/profile-photos/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::ProfilePhoto, + [=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/profile-photos/set-public"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::ProfilePhoto, + [=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); }, + u"privacy/set-public"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/profile-photos/update-public"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::ProfilePhoto, + [=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); }, + u"privacy/update-public"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/profile-photos/remove-public"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::ProfilePhoto, + [=] { return std::make_unique<::Settings::ProfilePhotoPrivacyController>(); }, + u"privacy/remove-public"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/bio"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::About, + [=] { return std::make_unique<::Settings::AboutPrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/bio/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::About, + [=] { return std::make_unique<::Settings::AboutPrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/bio/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::About, + [=] { return std::make_unique<::Settings::AboutPrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/gifts"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::GiftsAutoSave, + [=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/gifts/show-icon"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::GiftsAutoSave, + [=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); }, + u"privacy/show-icon"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/gifts/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::GiftsAutoSave, + [=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/gifts/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::GiftsAutoSave, + [=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/gifts/accepted-types"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::GiftsAutoSave, + [=] { return std::make_unique<::Settings::GiftsAutoSavePrivacyController>(); }, + u"privacy/accepted-types"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/birthday"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Birthday, + [=] { return std::make_unique<::Settings::BirthdayPrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/birthday/add"_q, + .action = CodeBlock{ [](const Context &ctx) { + return OpenInternalUrl(ctx, u"internal:edit_birthday"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/birthday/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Birthday, + [=] { return std::make_unique<::Settings::BirthdayPrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/birthday/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Birthday, + [=] { return std::make_unique<::Settings::BirthdayPrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/saved-music"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::SavedMusic, + [=] { return std::make_unique<::Settings::SavedMusicPrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/saved-music/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::SavedMusic, + [=] { return std::make_unique<::Settings::SavedMusicPrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/saved-music/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::SavedMusic, + [=] { return std::make_unique<::Settings::SavedMusicPrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/forwards"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::Forwards, + [=] { return std::make_unique<::Settings::ForwardsPrivacyController>(ctx.controller); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/forwards/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::Forwards, + [=] { return std::make_unique<::Settings::ForwardsPrivacyController>(ctx.controller); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/forwards/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::Forwards, + [=] { return std::make_unique<::Settings::ForwardsPrivacyController>(ctx.controller); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/calls"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Calls, + [=] { return std::make_unique<::Settings::CallsPrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/calls/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Calls, + [=] { return std::make_unique<::Settings::CallsPrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/calls/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Calls, + [=] { return std::make_unique<::Settings::CallsPrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/calls/p2p"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::CallsPeer2Peer, + [=] { return std::make_unique<::Settings::CallsPeer2PeerPrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/calls/p2p/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::CallsPeer2Peer, + [=] { return std::make_unique<::Settings::CallsPeer2PeerPrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/calls/p2p/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::CallsPeer2Peer, + [=] { return std::make_unique<::Settings::CallsPeer2PeerPrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/voice"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::Voices, + [=] { return std::make_unique<::Settings::VoicesPrivacyController>(&ctx.controller->session()); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/voice/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::Voices, + [=] { return std::make_unique<::Settings::VoicesPrivacyController>(&ctx.controller->session()); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/voice/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + return ShowPrivacyBox( + ctx, + PrivacyKey::Voices, + [=] { return std::make_unique<::Settings::VoicesPrivacyController>(&ctx.controller->session()); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/messages"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(Box(EditMessagesPrivacyBox, ctx.controller, QString())); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/messages/set-price"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(Box( + EditMessagesPrivacyBox, + ctx.controller, + u"privacy/set-price"_q)); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/messages/remove-fee"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(Box( + EditMessagesPrivacyBox, + ctx.controller, + u"privacy/remove-fee"_q)); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/invites"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Invites, + [=] { return std::make_unique<::Settings::GroupsInvitePrivacyController>(); }); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/invites/never"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Invites, + [=] { return std::make_unique<::Settings::GroupsInvitePrivacyController>(); }, + u"privacy/never"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/invites/always"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPrivacyBox( + ctx, + PrivacyKey::Invites, + [=] { return std::make_unique<::Settings::GroupsInvitePrivacyController>(); }, + u"privacy/always"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"privacy/self-destruct"_q, + .action = SettingsControl{ + ::Settings::PrivacySecurityId(), + u"privacy/self_destruct"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/data-settings/suggest-contacts"_q, + .action = SettingsControl{ + ::Settings::PrivacySecurityId(), + u"privacy/top_peers"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/data-settings/clear-payment-info"_q, + .action = SettingsControl{ + ::Settings::PrivacySecurityId(), + u"privacy/bots_payment"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"privacy/archive-and-mute"_q, + .action = SettingsControl{ + ::Settings::PrivacySecurityId(), + u"privacy/archive_and_mute"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"data/storage"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + LocalStorageBox::Show(ctx.controller); + return Result::Handled; + }}, + }); + router.add(u"settings"_q, { + .path = u"data/storage/clear-cache"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + LocalStorageBox::Show(ctx.controller, u"storage/clear-cache"_q); + return Result::Handled; + }}, + }); + router.add(u"settings"_q, { + .path = u"data/max-cache"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + LocalStorageBox::Show(ctx.controller, u"storage/max-cache"_q); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"data/show-18-content"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/show-18-content"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"data/proxy"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ProxiesBoxController::Show(ctx.controller); + return Result::Handled; + }}, + }); + router.add(u"settings"_q, { + .path = u"data/proxy/add-proxy"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ProxiesBoxController::Show(ctx.controller, u"proxy/add-proxy"_q); + return Result::Handled; + }}, + }); + router.add(u"settings"_q, { + .path = u"data/proxy/share-list"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ProxiesBoxController::Show(ctx.controller, u"proxy/share-list"_q); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance"_q, + .action = SettingsSection{ ::Settings::ChatId() }, + }); + + router.add(u"settings"_q, { + .path = u"power-saving"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPowerSavingBox(ctx); + }}, + }); + + router.add(u"settings"_q, { + .path = u"power-saving/stickers"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPowerSavingBox(ctx, PowerSaving::kStickersPanel); + }}, + }); + + router.add(u"settings"_q, { + .path = u"power-saving/emoji"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPowerSavingBox(ctx, PowerSaving::kEmojiPanel); + }}, + }); + + router.add(u"settings"_q, { + .path = u"power-saving/effects"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPowerSavingBox(ctx, PowerSaving::kChatBackground); + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance/themes"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/themes"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/themes/edit"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/themes-edit"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/themes/create"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/themes-create"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/wallpapers"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/wallpapers"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/wallpapers/set"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/wallpapers-set"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/wallpapers/choose-photo"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/wallpapers-choose-photo"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color"_q, + .action = AliasTo{ u"settings"_q, u"profile-color"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/profile"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/profile"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/profile/add-icons"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/profile/add-icons"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/profile/use-gift"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/profile/use-gift"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/profile/reset"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/profile/reset"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/name"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/name"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/name/add-icons"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/name/add-icons"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/your-color/name/use-gift"_q, + .action = AliasTo{ u"settings"_q, u"profile-color/name/use-gift"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/night-mode"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowMainMenuWithHighlight(ctx, u"main-menu/night-mode"_q); + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance/auto-night-mode"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/auto-night-mode"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/text-size"_q, + .action = SettingsControl{ + ::Settings::MainId(), + u"main/scale"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/animations"_q, + .action = AliasTo{ u"settings"_q, u"power-saving"_q }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/stickers-emoji"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/edit"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(Box( + ctx.controller->uiShow(), + StickersBox::Section::Installed)); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/trending"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(Box( + ctx.controller->uiShow(), + StickersBox::Section::Featured)); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/archived"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show(Box( + ctx.controller->uiShow(), + StickersBox::Section::Archived)); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/emoji"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show( + Box(&ctx.controller->session())); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/emoji/suggest"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/suggest-animated-emoji"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/emoji/quick-reaction"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/quick-reaction"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/emoji/quick-reaction/choose"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/quick-reaction-choose"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/suggest-by-emoji"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/suggest-by-emoji"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"appearance/stickers-and-emoji/emoji/large"_q, + .action = SettingsControl{ + ::Settings::ChatId(), + u"chat/large-emoji"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"language"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowLanguageBox(ctx); + }}, + .requiresAuth = false, + }); + + router.add(u"settings"_q, { + .path = u"language/show-button"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowLanguageBox(ctx, u"language/show-button"_q); + }}, + .requiresAuth = false, + }); + + router.add(u"settings"_q, { + .path = u"language/translate-chats"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowLanguageBox(ctx, u"language/translate-chats"_q); + }}, + .requiresAuth = false, + }); + + router.add(u"settings"_q, { + .path = u"language/do-not-translate"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowLanguageBox(ctx, u"language/do-not-translate"_q); + }}, + .requiresAuth = false, + }); + + router.add(u"settings"_q, { + .path = u"premium"_q, + .action = SettingsSection{ ::Settings::PremiumId() }, + }); + + router.add(u"settings"_q, { + .path = u"stars"_q, + .action = SettingsSection{ ::Settings::CreditsId() }, + }); + + router.add(u"settings"_q, { + .path = u"stars/top-up"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + static auto handler = ::Settings::BuyStarsHandler(); + handler.handler(ctx.controller->uiShow())(); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"stars/stats"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto self = ctx.controller->session().user(); + ctx.controller->showSection(Info::BotEarn::Make(self)); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"stars/gift"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + Ui::ShowGiftCreditsBox(ctx.controller, nullptr); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"stars/earn"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto self = ctx.controller->session().user(); + if (Info::BotStarRef::Join::Allowed(self)) { + ctx.controller->showSection(Info::BotStarRef::Join::Make(self)); + } + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"ton"_q, + .action = SettingsSection{ ::Settings::CurrencyId() }, + }); + + router.add(u"settings"_q, { + .path = u"business"_q, + .action = SettingsSection{ ::Settings::BusinessId() }, + }); + + router.add(u"settings"_q, { + .path = u"business/do-not-hide-ads"_q, + .action = SettingsControl{ + ::Settings::BusinessId(), + u"business/sponsored"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"send-gift"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + Ui::ChooseStarGiftRecipient(ctx.controller); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"send-gift/self"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + Ui::ShowStarGiftBox(ctx.controller, ctx.controller->session().user()); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"saved-messages"_q, + .action = CodeBlock{ ShowSavedMessages }, + }); + + router.add(u"settings"_q, { + .path = u"calls"_q, + .action = SettingsSection{ ::Settings::CallsId() }, + }); + + router.add(u"settings"_q, { + .path = u"calls/all"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + Calls::ShowCallsBox(ctx.controller); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"faq"_q, + .action = CodeBlock{ ShowFaq }, + .requiresAuth = false, + }); + + router.add(u"settings"_q, { + .path = u"ask-question"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ::Settings::OpenAskQuestionConfirm(ctx.controller); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"features"_q, + .action = CodeBlock{ [](const Context &ctx) { + UrlClickHandler::Open(tr::lng_telegram_features_url(tr::now)); + return Result::Handled; + }}, + .requiresAuth = false, + }); + + router.add(u"settings"_q, { + .path = u"search"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + const auto self = ctx.controller->session().user(); + auto stack = std::vector>(); + stack.push_back(std::make_shared( + self, + ::Settings::MainId())); + stack.push_back(std::make_shared( + self, + ::Settings::Search::Id())); + ctx.controller->showSection( + std::make_shared(std::move(stack))); + return Result::Handled; + }}, + }); + + router.add(u"settings"_q, { + .path = u"qr-code"_q, + .action = CodeBlock{ [](const Context &ctx) { + return HandleQrCode(ctx, false); + }}, + }); + + router.add(u"settings"_q, { + .path = u"qr-code/share"_q, + .action = CodeBlock{ [](const Context &ctx) { + return HandleQrCode(ctx, true); + }}, + }); + + // Edit profile deep links. + router.add(u"settings"_q, { + .path = u"edit/set-photo"_q, + .action = SettingsControl{ + ::Settings::MainId(), + u"profile-photo"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"edit/first-name"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowEditName(ctx, EditNameBox::Focus::FirstName); + }}, + }); + router.add(u"settings"_q, { + .path = u"edit/last-name"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowEditName(ctx, EditNameBox::Focus::LastName); + }}, + }); + router.add(u"settings"_q, { + .path = u"edit/bio"_q, + .action = SettingsControl{ + ::Settings::InformationId(), + u"edit/bio"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"edit/birthday"_q, + .action = CodeBlock{ [](const Context &ctx) { + return OpenInternalUrl(ctx, u"internal:edit_birthday"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"edit/change-number"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + ctx.controller->show( + Ui::MakeInformBox(tr::lng_change_phone_error())); + return Result::Handled; + }}, + }); + router.add(u"settings"_q, { + .path = u"edit/username"_q, + .action = CodeBlock{ ShowEditUsername }, + }); + router.add(u"settings"_q, { + .path = u"edit/your-color"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowPeerColorBox(ctx, PeerColorTab::Profile); + }}, + }); + router.add(u"settings"_q, { + .path = u"edit/channel"_q, + .action = SettingsControl{ + ::Settings::InformationId(), + u"edit/channel"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"edit/add-account"_q, + .action = SettingsControl{ + ::Settings::InformationId(), + u"edit/add-account"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"edit/log-out"_q, + .action = CodeBlock{ ShowLogOutMenu }, + }); + + // Calls deep links. + router.add(u"settings"_q, { + .path = u"calls/start-call"_q, + .action = CodeBlock{ [](const Context &ctx) { + if (!ctx.controller) { + return Result::NeedsAuth; + } + Calls::ShowCallsBox(ctx.controller, true); + return Result::Handled; + }}, + }); + + // Devices (sessions) deep links. + router.add(u"settings"_q, { + .path = u"devices/terminate-sessions"_q, + .action = SettingsControl{ + ::Settings::SessionsId(), + u"sessions/terminate-all"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"devices/auto-terminate"_q, + .action = SettingsControl{ + ::Settings::SessionsId(), + u"sessions/auto-terminate"_q, + }, + }); + + // Folders deep links. + router.add(u"settings"_q, { + .path = u"folders/create"_q, + .action = SettingsControl{ + ::Settings::FoldersId(), + u"folders/create"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"folders/add-recommended"_q, + .action = SettingsControl{ + ::Settings::FoldersId(), + u"folders/add-recommended"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"folders/show-tags"_q, + .action = SettingsControl{ + ::Settings::FoldersId(), + u"folders/show-tags"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"folders/tab-view"_q, + .action = SettingsControl{ + ::Settings::FoldersId(), + u"folders/tab-view"_q, + }, + }); + + // Notifications deep links. + router.add(u"settings"_q, { + .path = u"notifications/accounts"_q, + .action = SettingsControl{ + ::Settings::NotificationsId(), + u"notifications/accounts"_q, + }, + }); + + // Notification type deep links - Private Chats. + router.add(u"settings"_q, { + .path = u"notifications/private-chats"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType(ctx, Data::DefaultNotify::User); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/private-chats/edit"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType(ctx, Data::DefaultNotify::User); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/private-chats/show"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::User, + u"notifications/type/show"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/private-chats/sound"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::User, + u"notifications/type/sound"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/private-chats/add-exception"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::User, + u"notifications/type/add-exception"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/private-chats/delete-exceptions"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::User, + u"notifications/type/delete-exceptions"_q); + }}, + }); + + // Notification type deep links - Groups. + router.add(u"settings"_q, { + .path = u"notifications/groups"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType(ctx, Data::DefaultNotify::Group); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/groups/edit"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType(ctx, Data::DefaultNotify::Group); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/groups/show"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Group, + u"notifications/type/show"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/groups/sound"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Group, + u"notifications/type/sound"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/groups/add-exception"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Group, + u"notifications/type/add-exception"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/groups/delete-exceptions"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Group, + u"notifications/type/delete-exceptions"_q); + }}, + }); + + // Notification type deep links - Channels. + router.add(u"settings"_q, { + .path = u"notifications/channels"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType(ctx, Data::DefaultNotify::Broadcast); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/channels/edit"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType(ctx, Data::DefaultNotify::Broadcast); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/channels/show"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Broadcast, + u"notifications/type/show"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/channels/sound"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Broadcast, + u"notifications/type/sound"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/channels/add-exception"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Broadcast, + u"notifications/type/add-exception"_q); + }}, + }); + router.add(u"settings"_q, { + .path = u"notifications/channels/delete-exceptions"_q, + .action = CodeBlock{ [](const Context &ctx) { + return ShowNotificationType( + ctx, + Data::DefaultNotify::Broadcast, + u"notifications/type/delete-exceptions"_q); + }}, + }); + + // Other notification deep links. + router.add(u"settings"_q, { + .path = u"notifications/include-muted-chats"_q, + .action = SettingsControl{ + ::Settings::NotificationsId(), + u"notifications/include-muted-chats"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"notifications/count-unread-messages"_q, + .action = SettingsControl{ + ::Settings::NotificationsId(), + u"notifications/count-unread-messages"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"notifications/new-contacts"_q, + .action = SettingsControl{ + ::Settings::NotificationsId(), + u"notifications/events/joined"_q, + }, + }); + router.add(u"settings"_q, { + .path = u"notifications/pinned-messages"_q, + .action = SettingsControl{ + ::Settings::NotificationsId(), + u"notifications/events/pinned"_q, + }, + }); + + router.add(u"settings"_q, { + .path = u"themes"_q, + .action = AliasTo{ u"settings"_q, u"appearance/themes"_q }, + }); + + router.add(u"settings"_q, { + .path = u"themes/edit"_q, + .action = AliasTo{ u"settings"_q, u"appearance/themes/edit"_q }, + }); + + router.add(u"settings"_q, { + .path = u"themes/create"_q, + .action = AliasTo{ u"settings"_q, u"appearance/themes/create"_q }, + }); + + router.add(u"settings"_q, { + .path = u"change_number"_q, + .action = AliasTo{ u"settings"_q, u"edit/change-number"_q }, + }); + + router.add(u"settings"_q, { + .path = u"auto_delete"_q, + .action = AliasTo{ u"settings"_q, u"privacy/auto-delete"_q }, + }); + + router.add(u"settings"_q, { + .path = u"information"_q, + .action = AliasTo{ u"settings"_q, u"edit"_q }, + }); + + router.add(u"settings"_q, { + .path = u"edit_profile"_q, + .action = SettingsSection{ ::Settings::InformationId() }, + }); + + router.add(u"settings"_q, { + .path = u"phone_privacy"_q, + .action = AliasTo{ u"settings"_q, u"privacy/phone-number"_q }, + }); + + router.add(u"settings"_q, { + .path = u"login_email"_q, + .action = CodeBlock{ ShowLoginEmail }, + }); +} + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_settings.h b/Telegram/SourceFiles/core/deep_links/deep_links_settings.h new file mode 100644 index 0000000000..2a6fbb74c8 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_settings.h @@ -0,0 +1,16 @@ +/* +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 Core::DeepLinks { + +class Router; + +void RegisterSettingsHandlers(Router &router); + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_types.h b/Telegram/SourceFiles/core/deep_links/deep_links_types.h new file mode 100644 index 0000000000..b2e80efbe5 --- /dev/null +++ b/Telegram/SourceFiles/core/deep_links/deep_links_types.h @@ -0,0 +1,65 @@ +/* +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 "settings/settings_type.h" + +namespace Window { +class SessionController; +} // namespace Window + +namespace Core::DeepLinks { + +enum class Result { + Handled, + NotFound, + NeedsAuth, + Unsupported, +}; + +struct Context { + Window::SessionController *controller = nullptr; + QString section; + QString path; + QMap params; +}; + +using Handler = Fn; + +struct SettingsSection { + Settings::Type sectionId; +}; + +struct SettingsControl { + Settings::Type sectionId; + QString controlId; +}; + +struct CodeBlock { + Handler handler; +}; + +struct AliasTo { + QString section; + QString path; +}; + +using Action = std::variant< + SettingsSection, + SettingsControl, + CodeBlock, + AliasTo>; + +struct Entry { + QString path; + Action action; + bool requiresAuth = true; + bool skipActivation = false; +}; + +} // namespace Core::DeepLinks diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index 58d5fe8e04..2b109d6cf9 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -21,12 +21,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include namespace Core { namespace { uint64 InstallationTag = 0; +base::options::toggle OptionHighDpiDownscale({ + .id = kOptionHighDpiDownscale, + .name = "High DPI downscale", + .description = "Follow system interface scale settings exactly" + " (another approach, likely better quality).", + .scope = [] { + return !Platform::IsMac() + && QLibraryInfo::version() >= QVersionNumber(6, 4); + }, + .restartRequired = true, +}); + base::options::toggle OptionFreeType({ .id = kOptionFreeType, .name = "FreeType font engine", @@ -62,7 +75,7 @@ FilteredCommandLineArguments::FilteredCommandLineArguments( } #if defined Q_OS_WIN || defined Q_OS_MAC - if (OptionFreeType.value()) { + if (OptionFreeType.value() || OptionHighDpiDownscale.value()) { pushArgument("-platform"); #ifdef Q_OS_WIN pushArgument("windows:fontengine=freetype"); @@ -294,6 +307,7 @@ base::options::toggle OptionFractionalScalingEnabled({ } // namespace const char kOptionFractionalScalingEnabled[] = "fractional-scaling-enabled"; +const char kOptionHighDpiDownscale[] = "high-dpi-downscale"; const char kOptionFreeType[] = "freetype"; Launcher *Launcher::InstanceSetter::Instance = nullptr; @@ -345,7 +359,14 @@ void Launcher::initHighDpi() { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); #endif // Qt < 6.0.0 - if (OptionFractionalScalingEnabled.value()) { + if (OptionHighDpiDownscale.value()) { + qputenv("QT_WIDGETS_HIGHDPI_DOWNSCALE", "1"); + qputenv("QT_WIDGETS_RHI", "1"); + qputenv("QT_WIDGETS_RHI_BACKEND", "opengl"); + } + + if (OptionFractionalScalingEnabled.value() + || OptionHighDpiDownscale.value()) { QApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); } else { diff --git a/Telegram/SourceFiles/core/launcher.h b/Telegram/SourceFiles/core/launcher.h index e4de3c31d2..a0eb1e8d30 100644 --- a/Telegram/SourceFiles/core/launcher.h +++ b/Telegram/SourceFiles/core/launcher.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Core { extern const char kOptionFractionalScalingEnabled[]; +extern const char kOptionHighDpiDownscale[]; extern const char kOptionFreeType[]; class Launcher { diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 7bc0daee4b..ab4f62fb5d 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -7,8 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/local_url_handlers.h" -#include "api/api_authorizations.h" -#include "api/api_cloud_password.h" +#include "core/deep_links/deep_links_router.h" #include "api/api_confirm_phone.h" #include "api/api_chat_filters.h" #include "api/api_chat_invite.h" @@ -35,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/sticker_set_box.h" #include "boxes/star_gift_box.h" #include "boxes/language_box.h" +#include "boxes/url_auth_box.h" #include "passport/passport_form_controller.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -53,18 +53,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" #include "window/themes/window_theme_editor_box.h" // GenerateSlug. #include "payments/payments_checkout_process.h" -#include "settings/cloud_password/settings_cloud_password_login_email.h" -#include "settings/settings_active_sessions.h" -#include "settings/settings_credits.h" +#include "settings/sections/settings_credits.h" #include "settings/settings_credits_graphics.h" -#include "settings/settings_information.h" -#include "settings/settings_global_ttl.h" -#include "settings/settings_folders.h" -#include "settings/settings_main.h" #include "settings/settings_privacy_controllers.h" -#include "settings/settings_privacy_security.h" -#include "settings/settings_chat.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "storage/storage_account.h" #include "mainwidget.h" #include "main/main_account.h" @@ -310,42 +302,6 @@ void ShowLanguagesBox(Window::SessionController *controller) { Guard = LanguageBox::Show(controller); } -void ShowPhonePrivacyBox(Window::SessionController *controller) { - static auto Guard = base::binary_guard(); - auto guard = base::binary_guard(); - - using Privacy = Api::UserPrivacy; - const auto key = Privacy::Key::PhoneNumber; - controller->session().api().userPrivacy().reload(key); - - const auto weak = base::make_weak(controller); - auto shared = std::make_shared( - guard.make_guard()); - auto lifetime = std::make_shared(); - controller->session().api().userPrivacy().value( - key - ) | rpl::take( - 1 - ) | rpl::on_next([=](const Privacy::Rule &value) mutable { - using namespace ::Settings; - const auto show = shared->alive(); - if (lifetime) { - base::take(lifetime)->destroy(); - } - if (show) { - if (const auto controller = weak.get()) { - controller->show(Box( - controller, - std::make_unique( - controller), - value)); - } - } - }, *lifetime); - - Guard = std::move(guard); -} - bool SetLanguage( Window::SessionController *controller, const Match &match, @@ -503,40 +459,6 @@ bool ShowWallPaper( return result; } -void LoginEmailBox( - not_null box, - rpl::producer email, - Fn callback) { - { - box->getDelegate()->setTitle(rpl::duplicate( - email - ) | rpl::map([](QString email) { - if (email.contains(' ')) { - return tr::lng_settings_cloud_login_email_section_title( - tr::now, - tr::rich); - } - return Ui::Text::WrapEmailPattern(std::move(email)); - })); - for (const auto &child : ranges::views::reverse( - box->parentWidget()->children())) { - if (child && child->isWidgetType()) { - (static_cast(child))->setAttribute( - Qt::WA_TransparentForMouseEvents); - break; - } - } - } - Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ - .text = tr::lng_settings_cloud_login_email_box_about(), - .confirmed = [=](Fn close) { - callback(); - close(); - }, - .confirmText = tr::lng_settings_cloud_login_email_box_ok(), - }); -} - [[nodiscard]] ChatAdminRights ParseRequestedAdminRights( const QString &value) { auto result = ChatAdminRights(); @@ -615,6 +537,17 @@ bool ResolveUsernameOrPhone( ResolveGiftCode(controller, appnameParam, fromId, toId); return true; } + if (domainParam == u"oauth"_q) { + const auto token = params.value(u"startapp"_q); + if (!token.isEmpty()) { + UrlAuthBox::ActivateUrl( + controller->uiShow(), + &controller->session(), + u"tg://resolve?domain=oauth&startapp="_q + token, + context); + return true; + } + } // Fix t.me/s/username links. const auto webChannelPreviewLink = (domainParam == u"s"_q) @@ -798,68 +731,6 @@ bool ResolvePrivatePost( return true; } -void ShowLoginEmailSettings(Window::SessionController *controller) { - controller->session().api().cloudPassword().reload(); - controller->uiShow()->show(Box( - LoginEmailBox, - controller->session().api().cloudPassword().state( - ) | rpl::map([](const Core::CloudPasswordState &state) { - return state.loginEmailPattern; - }), - [=] { - controller->showSettings( - ::Settings::CloudLoginEmailId()); - controller->window().activate(); - })); -} - -bool ResolveSettings( - Window::SessionController *controller, - const Match &match, - const QVariant &context) { - const auto section = match->captured(1).mid(1).toLower(); - const auto type = [&]() -> std::optional<::Settings::Type> { - if (section == u"language"_q) { - ShowLanguagesBox(controller); - return {}; - } else if (section == u"phone_privacy"_q) { - ShowPhonePrivacyBox(controller); - return {}; - } else if (section == u"devices"_q) { - return ::Settings::Sessions::Id(); - } else if (section == u"folders"_q) { - return ::Settings::Folders::Id(); - } else if (section == u"privacy"_q) { - return ::Settings::PrivacySecurity::Id(); - } else if (section == u"themes"_q) { - return ::Settings::Chat::Id(); - } else if (section == u"change_number"_q) { - controller->show( - Ui::MakeInformBox(tr::lng_change_phone_error())); - return {}; - } else if (section == u"auto_delete"_q) { - return ::Settings::GlobalTTLId(); - } else if (section == u"information"_q) { - return ::Settings::Information::Id(); - } else if (section == u"login_email"_q) { - ShowLoginEmailSettings(controller); - return {}; - } - return ::Settings::Main::Id(); - }(); - - if (type.has_value()) { - if (!controller) { - return false; - } else if (*type == ::Settings::Sessions::Id()) { - controller->session().api().authorizations().reload(); - } - controller->showSettings(*type); - controller->window().activate(); - } - return true; -} - bool HandleUnknown( Window::SessionController *controller, const Match &match, @@ -1283,7 +1154,7 @@ bool EditPaidMessagesFee( ShowEditChatPermissions(controller, channel); } } else { - controller->show(Box(EditMessagesPrivacyBox, controller)); + controller->show(Box(EditMessagesPrivacyBox, controller, QString())); } return true; } @@ -1714,8 +1585,36 @@ bool ResolveTonSettings( return true; } +bool ResolveOAuth( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto params = url_parse_params( + match->captured(1), + qthelp::UrlParamNameTransform::ToLower); + const auto token = params.value(u"token"_q); + if (token.isEmpty()) { + return false; + } + UrlAuthBox::ActivateUrl( + controller->uiShow(), + &controller->session(), + u"tg://oauth?token="_q + token, + context); + return true; +} + } // namespace +bool TryRouterForLocalUrl( + Window::SessionController *controller, + const QString &command) { + return DeepLinks::Router::Instance().tryHandle(controller, command); +} + const std::vector &LocalUrlHandlers() { static auto Result = std::vector{ { @@ -1770,10 +1669,6 @@ const std::vector &LocalUrlHandlers() { u"^privatepost/?\\?(.+)(#|$)"_q, ResolvePrivatePost }, - { - u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile|/phone_privacy|/login_email)?$"_q, - ResolveSettings - }, { u"^test_chat_theme/?\\?(.+)(#|$)"_q, ResolveTestChatTheme, @@ -1826,6 +1721,10 @@ const std::vector &LocalUrlHandlers() { u"^ton/?(^\\?.*)?(#|$)"_q, ResolveTonSettings }, + { + u"^oauth/?\\?(.+)(#|$)"_q, + ResolveOAuth + }, { u"^user\\?(.+)(#|$)"_q, AyuUrlHandlers::ResolveUser @@ -2139,6 +2038,8 @@ void ResolveAndShowUniqueGift( const auto &data = result.data(); session->data().processUsers(data.vusers()); if (const auto gift = Api::FromTL(session, data.vgift())) { + Core::App().hideMediaView(); + using namespace ::Settings; show->show(Box( GlobalStarGiftBox, @@ -2146,6 +2047,7 @@ void ResolveAndShowUniqueGift( *gift, StarGiftResaleInfo(), st)); + show->activate(); } }).fail([=](const MTP::Error &error) { clear(); diff --git a/Telegram/SourceFiles/core/local_url_handlers.h b/Telegram/SourceFiles/core/local_url_handlers.h index ce71def0f7..bcdf1de241 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.h +++ b/Telegram/SourceFiles/core/local_url_handlers.h @@ -33,6 +33,10 @@ struct LocalUrlHandler { const QVariant &context)> handler; }; +[[nodiscard]] bool TryRouterForLocalUrl( + Window::SessionController *controller, + const QString &command); + [[nodiscard]] const std::vector &LocalUrlHandlers(); [[nodiscard]] const std::vector &InternalUrlHandlers(); diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 9b465ad6a7..1cd03ad86b 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -89,18 +89,24 @@ const auto kBadPrefix = u"http://"_q; QVariant context) { auto &account = Core::App().activeAccount(); const auto &config = account.appConfig(); + const auto my = context.value(); + const auto window = my.sessionWindow.get(); + const auto show = window + ? window->uiShow() + : my.show; const auto domains = config.get>( "url_auth_domains", {}); if (!account.sessionExists() || domain.isEmpty() + || !show || !ranges::contains(domains, domain)) { return false; } const auto good = url.startsWith(kBadPrefix, Qt::CaseInsensitive) ? (kGoodPrefix + url.mid(kBadPrefix.size())) : url; - UrlAuthBox::Activate(&account.session(), good, context); + UrlAuthBox::ActivateUrl(show, &account.session(), good, context); return true; } diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp index 2501a1364b..33c569c784 100644 --- a/Telegram/SourceFiles/core/update_checker.cpp +++ b/Telegram/SourceFiles/core/update_checker.cpp @@ -25,7 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "window/window_controller.h" #include "window/window_session_controller.h" -#include "settings/settings_advanced.h" +#include "settings/sections/settings_advanced.h" #include "settings/settings_intro.h" #include "ui/layers/box_content.h" @@ -1636,7 +1636,7 @@ void UpdateApplication() { controller->showSection( std::make_shared( Info::Settings::Tag{ controller->session().user() }, - ::Settings::Advanced::Id()), + ::Settings::AdvancedId()), Window::SectionShow()); } else { window->widget()->showSpecialLayer( diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index eb832a9f20..c67e7584d3 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 6004002; -constexpr auto AppVersionStr = "6.4.2"; +constexpr auto AppVersion = 6005001; +constexpr auto AppVersionStr = "6.5.1"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/components/gift_auctions.cpp b/Telegram/SourceFiles/data/components/gift_auctions.cpp index 33faaaad65..04a29580be 100644 --- a/Telegram/SourceFiles/data/components/gift_auctions.cpp +++ b/Telegram/SourceFiles/data/components/gift_auctions.cpp @@ -238,11 +238,11 @@ void GiftAuctions::requestAttributes(uint64 giftId, Fn ready) { }); } for (const auto &ready : base::take(entry.waiters)) { - ready(); + if (ready) ready(); } }).fail([=] { for (const auto &ready : base::take(_attributes[giftId].waiters)) { - ready(); + if (ready) ready(); } }).send(); } diff --git a/Telegram/SourceFiles/data/components/gift_auctions.h b/Telegram/SourceFiles/data/components/gift_auctions.h index a569b0fd01..5a24f3ab7e 100644 --- a/Telegram/SourceFiles/data/components/gift_auctions.h +++ b/Telegram/SourceFiles/data/components/gift_auctions.h @@ -92,7 +92,7 @@ public: [[nodiscard]] std::optional attributes( uint64 giftId) const; - void requestAttributes(uint64 giftId, Fn ready); + void requestAttributes(uint64 giftId, Fn ready = nullptr); [[nodiscard]] rpl::producer active() const; [[nodiscard]] rpl::producer hasActiveChanges() const; diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index dd271dc968..625a6e447c 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -568,7 +568,7 @@ void SponsoredMessages::append( .mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0), .mediaDocumentId = (mediaDocument ? mediaDocument->id : 0), .backgroundEmojiId = BackgroundEmojiIdFromColor(data.vcolor()), - .colorIndex = ColorIndexFromColor(data.vcolor()), + .colorIndex = ColorIndexFromColor(data.vcolor()).value_or(0), .isLinkInternal = !UrlRequiresConfirmation(qs(data.vurl())), .isRecommended = data.is_recommended(), .canReport = data.is_can_report(), diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 3a0c978f3e..b68e35d706 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -83,6 +83,7 @@ struct CreditsHistoryEntry final { Fn()> pinnedSavedGifts; uint64 nextToUpgradeStickerId = 0; Fn nextToUpgradeShow; + Fn craftAnotherCallback; CreditsAmount starrefAmount; int starrefCommission = 0; uint64 starrefRecipientId = 0; @@ -120,6 +121,7 @@ struct CreditsHistoryEntry final { bool giftResale : 1 = false; bool giftResaleForceTon : 1 = false; bool giftPinned : 1 = false; + bool giftCrafted : 1 = false; bool savedToProfile : 1 = false; bool fromGiftsList : 1 = false; bool fromGiftSlug : 1 = false; diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 93a991bfc3..1deb105a99 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -203,6 +203,7 @@ struct GiftCode { bool refunded : 1 = false; bool upgrade : 1 = false; bool saved : 1 = false; + bool craft : 1 = false; }; class Media { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index bff1ac2d30..599bf62750 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -433,7 +433,7 @@ void PeerData::paintUserpic( const auto cloud = userpicCloudImage(view); const auto ratio = style::DevicePixelRatio(); if (context.shape == Ui::PeerUserpicShape::Auto) { - context.shape = isForum() + context.shape = (isForum() && !isBot()) ? Ui::PeerUserpicShape::Forum : isMonoforum() ? Ui::PeerUserpicShape::Monoforum @@ -1025,8 +1025,9 @@ bool PeerData::changeColorCollectible( bool PeerData::changeColor( const tl::conditional &cloudColor) { - const auto changed1 = cloudColor - ? changeColorIndex(Data::ColorIndexFromColor(cloudColor)) + const auto maybeColorIndex = Data::ColorIndexFromColor(cloudColor); + const auto changed1 = maybeColorIndex + ? changeColorIndex(*maybeColorIndex) : clearColorIndex(); const auto changed2 = changeBackgroundEmojiId( Data::BackgroundEmojiIdFromColor(cloudColor)); @@ -1036,8 +1037,9 @@ bool PeerData::changeColor( bool PeerData::changeColorProfile( const tl::conditional &cloudColor) { - const auto changed1 = cloudColor - ? changeColorProfileIndex(Data::ColorIndexFromColor(cloudColor)) + const auto maybeColorIndex = Data::ColorIndexFromColor(cloudColor); + const auto changed1 = maybeColorIndex + ? changeColorProfileIndex(*maybeColorIndex) : clearColorProfileIndex(); const auto changed2 = changeProfileBackgroundEmojiId( Data::BackgroundEmojiIdFromColor(cloudColor)); @@ -1262,7 +1264,7 @@ not_null PeerData::userpicPaintingPeer() const { } Ui::PeerUserpicShape PeerData::userpicShape() const { - return isForum() + return isForum() && !isBot() ? Ui::PeerUserpicShape::Forum : isMonoforum() ? Ui::PeerUserpicShape::Monoforum @@ -2188,17 +2190,22 @@ uint64 BackgroundEmojiIdFromColor(const MTPPeerColor *color) { }); } -uint8 ColorIndexFromColor(const MTPPeerColor *color) { +std::optional ColorIndexFromColor(const MTPPeerColor *color) { if (!color) { - return 0; + return std::nullopt; } - return color->match([](const MTPDpeerColor &data) -> uint8 { - return data.vcolor().value_or_empty(); - }, [](const MTPDpeerColorCollectible &data) -> uint8 { - return 0; - }, [](const MTPDinputPeerColorCollectible &) -> uint8 { - return 0; + return color->match([](const MTPDpeerColor &d) -> std::optional { + return d.vcolor() ? std::make_optional(d.vcolor()->v) : std::nullopt; + }, [](const auto &) -> std::optional { + return std::nullopt; }); } +bool IsBotCanManageTopics(not_null peer) { + if (const auto user = peer->asUser()) { + return user->botInfo && user->botInfo->canManageTopics; + } + return false; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index b320cd15cb..9daad00e25 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -676,6 +676,8 @@ void SetTopPinnedMessageId( PeerData *migrated = nullptr); [[nodiscard]] uint64 BackgroundEmojiIdFromColor(const MTPPeerColor *color); -[[nodiscard]] uint8 ColorIndexFromColor(const MTPPeerColor *color); +[[nodiscard]] std::optional ColorIndexFromColor(const MTPPeerColor *); + +[[nodiscard]] bool IsBotCanManageTopics(not_null); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 3b0b9077df..4fb3c39357 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -767,6 +767,8 @@ not_null Session::processUser(const MTPUser &data) { result->botInfo->canEditInformation = data.is_bot_can_edit(); result->botInfo->activeUsers = data.vbot_active_users().value_or_empty(); result->botInfo->hasMainApp = data.is_bot_has_main_app(); + result->botInfo->canManageTopics + = data.is_bot_forum_can_manage_topics(); } else { result->setBotInfoVersion(-1); } @@ -1118,9 +1120,11 @@ not_null Session::processChat(const MTPChat &data) { using Flag = ChannelDataFlag; const auto flagsMask = Flag::Broadcast | Flag::Megagroup - | Flag::Forbidden; + | Flag::Forbidden + | Flag::Monoforum; const auto flagsSet = (data.is_broadcast() ? Flag::Broadcast : Flag()) | (data.is_megagroup() ? Flag::Megagroup : Flag()) + | (data.is_monoforum() ? Flag::Monoforum : Flag()) | Flag::Forbidden; channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index eb460302b6..d4096e7845 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -104,6 +104,7 @@ struct GiftUpdate { Pin, Unpin, ResaleChange, + Upgraded, }; Data::SavedStarGiftId id; diff --git a/Telegram/SourceFiles/data/data_star_gift.cpp b/Telegram/SourceFiles/data/data_star_gift.cpp index adebd29916..06b0bc5f60 100644 --- a/Telegram/SourceFiles/data/data_star_gift.cpp +++ b/Telegram/SourceFiles/data/data_star_gift.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" +#include "lang/lang_keys.h" #include "lang/lang_tag.h" #include "main/main_session.h" #include "ui/controls/ton_common.h" @@ -23,6 +24,7 @@ namespace { constexpr auto kMyGiftsPerPage = 50; constexpr auto kResaleGiftsPerPage = 50; +constexpr auto kCraftGiftsPerPage = 50; [[nodiscard]] MTPStarGiftAttributeId AttributeToTL(GiftAttributeId id) { switch (id.type) { @@ -63,7 +65,52 @@ QString UniqueGiftName(const UniqueGift &gift) { } QString UniqueGiftName(const QString &title, int number) { - return title + u" #"_q + QString::number(number); + return title + u" #"_q + Lang::FormatCountDecimal(number); +} + +QString UniqueGiftRarityText(UniqueGiftRarity type) { + switch (type) { + case UniqueGiftRarity::Uncommon: + return tr::lng_gift_uncommon_tag(tr::now); + case UniqueGiftRarity::Rare: + return tr::lng_gift_rare_tag(tr::now); + case UniqueGiftRarity::Epic: + return tr::lng_gift_epic_tag(tr::now); + case UniqueGiftRarity::Legendary: + return tr::lng_gift_legendary_tag(tr::now); + } + return QString(); +} + +UniqueGiftRarityColors UniqueGiftRarityBadgeColors(UniqueGiftRarity type) { + constexpr auto kBgOpacity = 0.15; + const auto base = [&] { + switch (type) { + case UniqueGiftRarity::Uncommon: return QColor(0x40, 0xA9, 0x20); + case UniqueGiftRarity::Rare: return QColor(0x11, 0xAA, 0xBE); + case UniqueGiftRarity::Epic: return QColor(0x95, 0x5C, 0xDB); + case UniqueGiftRarity::Legendary: return QColor(0xBF, 0x76, 0x00); + } + Unexpected("Invalid rarity type."); + }(); + auto bg = base; + bg.setAlphaF(kBgOpacity); + return { bg, base }; +} + +QString UniqueGiftAttributeText(const UniqueGiftAttribute &attribute) { + const auto type = attribute.rarityType(); + if (type != UniqueGiftRarity::Default) { + return UniqueGiftRarityText(type); + } + const auto permille = attribute.rarityPermille(); + return (permille > 0) + ? (QString::number(permille / 10.) + '%') + : u"< 0.1%"_q; +} + +bool UniqueGiftAttributeHasSpecialRarity(const UniqueGiftAttribute &attribute) { + return attribute.rarityType() != UniqueGiftRarity::Default; } CreditsAmount UniqueGiftResaleStars(const UniqueGift &gift) { @@ -188,7 +235,8 @@ rpl::producer ResaleGiftsSlice( : Flag()) | (filter.attributes.empty() ? Flag() - : Flag::f_attributes)), + : Flag::f_attributes) + | (filter.forCraft ? Flag::f_for_craft : Flag())), MTP_long(filter.attributesHash), MTP_long(giftId), MTP_vector_from_range(filter.attributes @@ -258,4 +306,45 @@ rpl::producer ResaleGiftsSlice( }; } +rpl::producer CraftGiftsSlice( + not_null session, + uint64 giftId, + QString offset) { + return [=](auto consumer) { + const auto requestId = session->api().request( + MTPpayments_GetCraftStarGifts( + MTP_long(giftId), + MTP_string(offset), + MTP_int(kCraftGiftsPerPage)) + ).done([=](const MTPpayments_SavedStarGifts &result) { + const auto user = session->user(); + const auto owner = &session->data(); + const auto &data = result.data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + + auto info = CraftGiftsDescriptor{ + .giftId = giftId, + .offset = qs(data.vnext_offset().value_or_empty()), + .count = data.vcount().v, + }; + info.list.reserve(data.vgifts().v.size()); + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(user, gift)) { + info.list.push_back(std::move(*parsed)); + } + } + consumer.put_next(std::move(info)); + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_next({}); + consumer.put_done(); + }).send(); + + auto lifetime = rpl::lifetime(); + lifetime.add([=] { session->api().request(requestId).cancel(); }); + return lifetime; + }; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index cba0f2fded..7cd0b73cca 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -17,15 +17,45 @@ struct ColorCollectible; namespace Data { +enum class UniqueGiftRarity : int { + Default = 0, + Uncommon = -1, + Rare = -2, + Epic = -3, + Legendary = -4, +}; + struct UniqueGiftAttribute { QString name; - int rarityPermille = 0; + int rarityValue = 0; + + [[nodiscard]] UniqueGiftRarity rarityType() const { + return (rarityValue >= 0) + ? UniqueGiftRarity::Default + : UniqueGiftRarity(rarityValue); + } + [[nodiscard]] int rarityPermille() const { + return (rarityValue >= 0) ? rarityValue : 0; + } friend inline bool operator==( const UniqueGiftAttribute &, const UniqueGiftAttribute &) = default; }; +struct UniqueGiftRarityColors { + QColor bg; + QColor fg; +}; + +[[nodiscard]] QString UniqueGiftRarityText(UniqueGiftRarity type); +[[nodiscard]] UniqueGiftRarityColors UniqueGiftRarityBadgeColors( + UniqueGiftRarity type); +[[nodiscard]] QString UniqueGiftAttributeText( + const UniqueGiftAttribute &attribute); +[[nodiscard]] bool UniqueGiftAttributeHasSpecialRarity( + const UniqueGiftAttribute &attribute); + struct UniqueGiftModel : UniqueGiftAttribute { not_null document; @@ -97,15 +127,19 @@ struct UniqueGift { PeerData *releasedBy = nullptr; PeerData *themeUser = nullptr; int64 nanoTonForResale = -1; + int craftChancePermille = 0; int starsForResale = -1; int starsForTransfer = -1; int starsMinOffer = -1; int number = 0; bool onlyAcceptTon = false; bool canBeTheme = false; + bool crafted = false; + bool burned = false; TimeId exportAt = 0; TimeId canTransferAt = 0; TimeId canResellAt = 0; + TimeId canCraftAt = 0; UniqueGiftModel model; UniqueGiftPattern pattern; UniqueGiftBackdrop backdrop; @@ -362,6 +396,7 @@ struct ResaleGiftsFilter { uint64 attributesHash = 0; base::flat_set attributes; ResaleGiftsSort sort = ResaleGiftsSort::Price; + bool forCraft = false; friend inline bool operator==( const ResaleGiftsFilter &, @@ -374,4 +409,16 @@ struct ResaleGiftsFilter { ResaleGiftsFilter filter = {}, QString offset = QString()); +struct CraftGiftsDescriptor { + uint64 giftId = 0; + QString offset; + std::vector list; + int count = 0; +}; + +[[nodiscard]] rpl::producer CraftGiftsSlice( + not_null session, + uint64 giftId, + QString offset = QString()); + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 82da8db089..3f71f50473 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -2274,11 +2274,6 @@ void Stories::togglePinnedList( auto &saved = _saved[peerId]; auto list = QVector(); list.reserve(maxPinnedCount()); - for (const auto &id : saved.ids.pinnedToTop) { - if (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) { - list.push_back(MTP_int(id)); - } - } if (pin) { auto copy = ids; ranges::sort(copy, ranges::greater()); @@ -2289,6 +2284,11 @@ void Stories::togglePinnedList( } } } + for (const auto &id : saved.ids.pinnedToTop) { + if (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) { + list.push_back(MTP_int(id)); + } + } const auto api = &_owner->session().api(); const auto peer = session().data().peer(peerId); api->request(MTPstories_TogglePinnedToTop( diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 6a5410a008..4243548d93 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -98,6 +98,7 @@ struct BotInfo { bool canManageEmojiStatus : 1 = false; bool supportsBusiness : 1 = false; bool hasMainApp : 1 = false; + bool canManageTopics : 1 = false; private: std::unique_ptr _forum; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index d03fa7d548..2186c001f6 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -271,7 +271,10 @@ bool WebPageData::applyChanges( const auto hasSiteName = !resultSiteName.isEmpty() ? 1 : 0; const auto hasTitle = !resultTitle.isEmpty() ? 1 : 0; const auto hasDescription = !newDescription.text.isEmpty() ? 1 : 0; - if (newDocument + const auto allowLargeMediaDocument = newDocument + && newDocument->isVideoFile() + && newPhoto; + if ((!allowLargeMediaDocument && newDocument) || !newCollage.items.empty() || !newPhoto || (hasSiteName + hasTitle + hasDescription < 2)) { diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index af551ef4c4..d6f3549fc5 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -22,7 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "apiwrap.h" #include "storage/storage_account.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "core/application.h" #include "core/core_settings.h" #include "main/main_session.h" diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 2fbbfc215c..5e3c072846 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -77,6 +77,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/unread_badge.h" #include "boxes/filters/edit_filter_box.h" #include "boxes/peers/edit_forum_topic_box.h" +#include "boxes/peer_list_box.h" #include "api/api_chat_filters.h" #include "base/qt/qt_common_adapters.h" #include "styles/style_dialogs.h" @@ -89,6 +90,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" #include +#include // AyuGram includes #include "ayu/utils/telegram_helpers.h" @@ -100,6 +102,7 @@ namespace { constexpr auto kHashtagResultsLimit = 5; constexpr auto kStartReorderThreshold = 30; +constexpr auto kStartDragToFilterThreshold = 35; constexpr auto kQueryPreviewLimit = 32; constexpr auto kPreviewPostsLimit = 3; @@ -1739,12 +1742,81 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { && *_lastMousePosition == globalPosition) { return; } + + if (_pressed && (e->buttons() & Qt::LeftButton)) { + const auto local = e->pos(); + const auto outside = _dragging ? !rect().contains(local) : true; + const auto distanceExceeded = (local - _dragStart).manhattanLength() + >= style::ConvertScale(kStartDragToFilterThreshold); + + if (!_qdragging && outside && distanceExceeded) { + if (_pressed->history()) { + _dragging = _pressed; + _qdragging = _pressed; + InvokeQueued(this, [=] { performDrag(); }); + return; + } + } else if (!outside && _qdragging) { + _qdragging = nullptr; + } + } + selectByMouse(globalPosition); if (_chatPreviewScheduled && !isUserpicPress()) { cancelChatPreview(); } } +void InnerWidget::performDrag() { + if (!_qdragging) { + return; + } + const auto history = _qdragging->history(); + if (!history) { + return; + } + + auto mimeData = std::make_unique(); + auto byteArray = [&] { + auto data = QByteArray(); + auto stream = QDataStream(&data, QIODevice::WriteOnly); + stream << history->peer->id.value; + stream << history->session().isTestMode(); + return data; + }(); + mimeData->setData( + u"application/x-telegram-dialog"_q, + std::move(byteArray)); + + const auto &st = st::defaultDialogRow; + auto pixmap = QPixmap(Size(st.height * style::DevicePixelRatio())); + pixmap.setDevicePixelRatio(style::DevicePixelRatio()); + pixmap.fill(Qt::transparent); + if (const auto draw = PaintUserpicCallback(history->peer, true)) { + auto p = Painter(&pixmap); + p.setOpacity(0.7); + const auto pos = (st.height - st.photoSize) / 2; + draw(p, pos, pos, st.height, st.photoSize); + } + + Ui::Animations::Manager::SetScheduleWithInvokeQueued(true); + + _controller->cancelScheduledPreview(); + // This call enters event loop and can destroy any QObject. + _controller->widget()->launchDrag( + std::move(mimeData), + [=, weak = base::make_weak(this)] { + Ui::Animations::Manager::SetScheduleWithInvokeQueued(false); + if (weak) { + _qdragging = nullptr; + clearPressed(); + finishReorderOnRelease(); + selectByMouse(QCursor::pos()); + } + }, + pixmap); +} + void InnerWidget::cancelChatPreview() { _chatPreviewTouchGlobal = {}; _chatPreviewScheduled = false; @@ -2347,6 +2419,7 @@ void InnerWidget::finishReorderPinned() { savePinnedOrder(); _dragging = nullptr; _touchDragStartGlobal = {}; + _qdragging = nullptr; } _draggingIndex = -1; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 9ee41b58be..621847e651 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -527,6 +527,8 @@ private: uint8 more, bool active); + void performDrag(); + const not_null _controller; not_null _shownList; @@ -562,6 +564,8 @@ private: bool _selectedRightButton = false; bool _pressedRightButton = false; + Row *_qdragging = nullptr; + Row *_dragging = nullptr; int _draggingIndex = -1; int _aboveIndex = -1; diff --git a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp index d62aef34a0..5406b7947d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp @@ -30,9 +30,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "settings/settings_active_sessions.h" +#include "settings/sections/settings_active_sessions.h" #include "settings/settings_credits_graphics.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/effects/credits_graphics.h" @@ -97,7 +97,7 @@ void ShowAuthToast( Qt::MouseButton button) { if (const auto controller = FindSessionController(parent)) { session->api().authorizations().reload(); - controller->showSettings(Settings::Sessions::Id()); + controller->showSettings(Settings::SessionsId()); return false; } return true; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 08c34fd108..6445df6291 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -465,7 +465,6 @@ Widget::Widget( _scrollToTop->raise(); _lockUnlock->toggle(false, anim::type::instant); - _inner->updated( ) | rpl::on_next([=] { listScrollUpdated(); @@ -1656,7 +1655,9 @@ void Widget::updateControlsVisibility(bool fast) { _forumReportBar->show(); } } else { - updateLockUnlockVisibility(); + updateLockUnlockVisibility(fast + ? anim::type::instant + : anim::type::normal); updateJumpToDateVisibility(fast); updateSearchFromVisibility(fast); } @@ -2393,7 +2394,7 @@ void Widget::stopWidthAnimation() { } void Widget::updateStoriesVisibility() { - updateLockUnlockVisibility(); + updateLockUnlockVisibility(anim::type::normal); if (!_stories) { return; } @@ -2404,17 +2405,24 @@ void Widget::updateStoriesVisibility() { return; } - const auto hidden = (_showAnimation != nullptr) + const auto widthAnimation = !_widthAnimationCache.isNull(); + const auto suggestionsAnimation = widthAnimation + && (!_suggestions || !_hidingSuggestions.empty()); + const auto hiddenInstant = _showAnimation || _openedForum - || !_widthAnimationCache.isNull() + || (widthAnimation && !suggestionsAnimation) || _childList - || _searchHasFocus + || _stories->empty() + || (_scroll->position().overscroll < -st::dialogsFilterSkip); + const auto hiddenAnimated = _searchHasFocus || _searchSuggestionsLocked || !_searchState.query.isEmpty() || _searchState.inChat - || _stories->empty(); - if (_stories->isHidden() != hidden) { - _stories->setVisible(!hidden); + || suggestionsAnimation; + const auto hidden = hiddenInstant || hiddenAnimated; + const auto changed = (_stories->toggledHidden() != hidden); + _stories->setToggledHidden(hiddenInstant, hiddenAnimated); + if (changed) { using Type = Ui::ElasticScroll::OverscrollType; if (hidden) { _scroll->setOverscrollDefaults(0, 0); @@ -2479,7 +2487,7 @@ void Widget::startSlideAnimation( Window::SlideDirection direction) { _scroll->hide(); if (_stories) { - _stories->hide(); + _stories->setToggledHidden(true, false); } _searchControls->hide(); if (_subsectionTopBar) { @@ -2886,9 +2894,30 @@ void Widget::searchMessages(SearchState state) { if (_openedForum && peer->forum() != _openedForum) { controller()->closeForum(); } + } else if (state.query.isEmpty()) { + if (_childList) { + hideChildList(); + } + if (_openedForum) { + controller()->closeForum(); + } + if (_layout == Layout::Main) { + controller()->closeFolder(); + } } applySearchState(std::move(state)); session().local().saveRecentSearchHashtags(_searchState.query); + + if (_childList) { + _childList->setInnerFocus(); + } else if (_subsectionTopBar) { + if (!_subsectionTopBar->searchSetFocus() + && !_subsectionTopBar->searchHasFocus()) { + _subsectionTopBar->toggleSearch(true, anim::type::normal); + } + } else { + _search->setFocus(); + } } void Widget::searchTopics() { @@ -3161,7 +3190,7 @@ void Widget::searchReceived( process->queries.erase(i); } } - const auto inject = (type.start && !type.posts) + const auto inject = (type.start && !type.posts && !type.migrated) ? *_singleMessageSearch.lookup(_searchQuery) : nullptr; if (cacheResults && process->requestId) { @@ -3911,20 +3940,28 @@ void Widget::updateLockUnlockVisibility(anim::type animated) { if (_showAnimation) { return; } - const auto hidden = !session().domain().local().hasLocalPasscode() - || _showAnimation + const auto widthAnimation = !_widthAnimationCache.isNull(); + const auto suggestionsAnimation = widthAnimation + && (!_suggestions || !_hidingSuggestions.empty()); + const auto hiddenInstant = _showAnimation || _openedForum - || !_widthAnimationCache.isNull() + || (widthAnimation && !suggestionsAnimation) || _childList - || _searchHasFocus + || !session().domain().local().hasLocalPasscode() + || (_stories + && !_stories->empty() + && _scroll->position().overscroll < -st::dialogsFilterSkip); + const auto hiddenAnimated = _searchHasFocus || _searchSuggestionsLocked + || !_searchState.query.isEmpty() || _searchState.inChat - || !_searchState.query.isEmpty(); - if (_lockUnlock->toggled() == hidden) { - const auto stories = _stories && !_stories->empty(); - _lockUnlock->toggle( - !hidden, - stories ? anim::type::instant : animated); + || suggestionsAnimation; + const auto hidden = hiddenInstant || hiddenAnimated; + const auto changed = (_lockUnlock->toggled() == hidden); + _lockUnlock->toggle( + !hidden, + hiddenInstant ? anim::type::instant : animated); + if (changed) { if (!hidden) { updateLockUnlockPosition(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 24d924e5f6..a6869c14c1 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -199,6 +199,38 @@ rpl::producer> List::verticalScrollEvents() const { return _verticalScrollEvents.events(); } +bool List::toggledHidden() const { + return _hiddenInstant || _hiddenAnimated; +} + +void List::setToggledHidden(bool hiddenInstant, bool hiddenAnimated) { + const auto hidden = (hiddenInstant || hiddenAnimated); + const auto hiddenChanged = (hidden != toggledHidden()); + const auto hiddenInstantChanged = (_hiddenInstant != hiddenInstant); + const auto hiddenAnimatedChanged = (_hiddenAnimated != hiddenAnimated); + _hiddenInstant = hiddenInstant; + _hiddenAnimated = hiddenAnimated; + if (hiddenChanged) { + if (_hiddenInstant || !hiddenAnimatedChanged) { + _hiddenAnimation.stop(); + setVisible(!toggledHidden()); + } else { + const auto from = hidden ? 0. : 1.; + const auto till = hidden ? 1. : 0.; + _hiddenAnimation.start([=] { + if (!_hiddenAnimation.animating()) { + setVisible(!toggledHidden()); + } + update(); + }, from, till, st::fadeWrapDuration, anim::linear); + show(); + } + } else if (hiddenInstantChanged && _hiddenInstant) { + _hiddenAnimation.stop(); + setVisible(!toggledHidden()); + } +} + void List::requestExpanded(bool expanded) { if (_expanded != expanded) { _expanded = expanded; @@ -341,6 +373,10 @@ List::Layout List::computeLayout(float64 expanded) const { } void List::paintEvent(QPaintEvent *e) { + const auto hidden = _hiddenAnimation.value(toggledHidden() ? 1. : 0.); + if (hidden >= 1.) { + return; + } const auto &st = _st.small; const auto &full = _st.full; const auto layout = computeLayout(); @@ -354,13 +390,18 @@ void List::paintEvent(QPaintEvent *e) { }; const auto line = elerp(st.lineTwice, full.lineTwice) / 2.; const auto photo = lerp(st.photo, full.photo); - const auto layered = layout.single < (photo + 4 * line); + const auto layered = (layout.single < (photo + 4 * line)) + || (hidden > 0.); auto p = QPainter(this); if (layered) { ensureLayer(); auto q = QPainter(&_layer); paint(q, layout, photo, line, true); q.end(); + + if (hidden > 0.) { + p.setOpacity(1. - hidden); + } p.drawImage(0, 0, _layer); } else { paint(p, layout, photo, line, false); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index f55151ca4f..64ac522bb0 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -101,6 +101,9 @@ public: [[nodiscard]] auto verticalScrollEvents() const -> rpl::producer>; + [[nodiscard]] bool toggledHidden() const; + void setToggledHidden(bool hiddenInstant, bool hiddenAnimated); + private: struct Layout; enum class State { @@ -196,8 +199,11 @@ private: Ui::Animations::Simple _expandedAnimation; Ui::Animations::Simple _expandCatchUpAnimation; + Ui::Animations::Simple _hiddenAnimation; float64 _lastRatio = 0.; int _lastExpandedHeight = 0; + bool _hiddenAnimated : 1 = false; + bool _hiddenInstant : 1 = false; bool _expandIgnored : 1 = false; bool _expanded : 1 = false; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index a72c03ad84..b6d244b8db 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -43,7 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "settings/settings_common.h" #include "settings/settings_credits_graphics.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "storage/storage_shared_media.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/swipe_handler.h" diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index a25a384152..7af24dd128 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1221,6 +1221,7 @@ Chat ParseChat(const MTPChat &data) { result.bareId = data.vid().v; result.isBroadcast = data.is_broadcast(); result.isSupergroup = data.is_megagroup(); + result.isMonoforum = data.is_monoforum(); result.title = ParseString(data.vtitle()); result.input = MTP_inputPeerChannel( MTP_long(result.bareId), @@ -1848,6 +1849,14 @@ ServiceAction ParseServiceAction( content.offerExpired = data.is_expired(); content.offerPrice = CreditsAmountFromTL(data.vprice()); result.content = content; + }, [&](const MTPDmessageActionNewCreatorPending &data) { + auto content = ActionNewCreatorPending(); + content.newCreatorId = data.vnew_creator_id().v; + result.content = content; + }, [&](const MTPDmessageActionChangeCreator &data) { + auto content = ActionChangeCreator(); + content.newCreatorId = data.vnew_creator_id().v; + result.content = content; }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 8a4d0060a7..dc6cb4b8bf 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -738,6 +738,14 @@ struct ActionSuggestBirthday { Birthday birthday; }; +struct ActionNewCreatorPending { + UserId newCreatorId = 0; +}; + +struct ActionChangeCreator { + UserId newCreatorId = 0; +}; + struct ServiceAction { std::variant< v::null_t, @@ -791,7 +799,9 @@ struct ServiceAction { ActionSuggestedPostApproval, ActionSuggestedPostSuccess, ActionSuggestedPostRefund, - ActionSuggestBirthday> content; + ActionSuggestBirthday, + ActionNewCreatorPending, + ActionChangeCreator> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 008705fd47..15417fa1a2 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1553,6 +1553,16 @@ auto HtmlWriter::Wrap::pushMessage( }() + (data.birthday.year() ? (' ' + QByteArray::number(data.birthday.year())) : QByteArray()); + }, [&](const ActionNewCreatorPending &data) { + return peers.wrapUserName(data.newCreatorId) + + " will become the new main admin in 7 days if " + + serviceFrom + + " does not return"; + }, [&](const ActionChangeCreator &data) { + return serviceFrom + + " made " + + peers.wrapUserName(data.newCreatorId) + + " the new main admin of the group"; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 18cb7e4fbe..434c69ebc9 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -743,6 +743,14 @@ QByteArray SerializeMessage( if (const auto year = data.birthday.year()) { push("year", year); } + }, [&](const ActionNewCreatorPending &data) { + pushActor(); + pushAction("new_creator_pending"); + pushBare("new_creator", wrapUserName(data.newCreatorId)); + }, [&](const ActionChangeCreator &data) { + pushActor(); + pushAction("change_creator"); + pushBare("new_creator", wrapUserName(data.newCreatorId)); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp index fc78769c81..9d5222d6eb 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp @@ -104,7 +104,7 @@ Fn FillFilterValueList( not_null container, bool isChannel, const FilterValue &filter) { - auto [checkboxes, getResult, changes] = CreateEditAdminLogFilter( + auto [checkboxes, getResult, changes, highlightWidget] = CreateEditAdminLogFilter( container, filter.flags ? (*filter.flags) : ~FilterValue::Flags(0), isChannel); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 480c1a626d..fc6b7963b1 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -1306,9 +1306,7 @@ void History::applyServiceChanges( .text = tr::lng_payments_success( tr::now, lt_amount, - Ui::Text::Wrapped( - payment->amount, - EntityType::Bold), + Ui::Text::Wrapped(payment->amount, EntityType::Bold), lt_title, tr::bold(paid->title), tr::marked), @@ -4004,6 +4002,11 @@ void History::clear(ClearType type, bool markEmpty) { } else if (const auto channel = peer->asMegagroup()) { channel->mgInfo->markupSenders.clear(); } + if (const auto forum = peer->forum()) { + forum->enumerateTopics([&](not_null topic) { + destroyMessagesByTopic(topic->rootId()); + }); + } owner().notifyHistoryChangeDelayed(this); owner().sendHistoryChangeNotifications(); diff --git a/Telegram/SourceFiles/history/history_drag_area.cpp b/Telegram/SourceFiles/history/history_drag_area.cpp index f407c573d2..cbfd5e74d2 100644 --- a/Telegram/SourceFiles/history/history_drag_area.cpp +++ b/Telegram/SourceFiles/history/history_drag_area.cpp @@ -93,6 +93,7 @@ DragArea::Areas DragArea::SetupDragAreaToContainer( moveToTop(attachDragDocument); break; case DragState::PhotoFiles: + case DragState::MediaFiles: attachDragDocument->resize( width() - horizontalMargins, (height() - verticalMargins) / 2); @@ -147,6 +148,20 @@ DragArea::Areas DragArea::SetupDragAreaToContainer( attachDragDocument->otherEnter(); attachDragPhoto->otherEnter(); break; + case DragState::MediaFiles: + attachDragDocument->setText( + tr::lng_drag_files_here(tr::now), + hideSubtext + ? QString() + : tr::lng_drag_to_send_files(tr::now)); + attachDragPhoto->setText( + tr::lng_drag_media_here(tr::now), + hideSubtext + ? QString() + : tr::lng_drag_to_send_media(tr::now)); + attachDragDocument->otherEnter(); + attachDragPhoto->otherEnter(); + break; case DragState::Image: attachDragPhoto->setText( tr::lng_drag_images_here(tr::now), diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 15b658877d..b9fa929eeb 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_reaction_preview.h" #include "history/view/history_view_quick_action.h" #include "history/view/history_view_emoji_interactions.h" +#include "history/view/history_view_top_peers_selector.h" #include "history/history_item_components.h" #include "history/history_item_text.h" #include "payments/payments_reaction_process.h" @@ -2374,6 +2375,28 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { e, session().data().reactions().favoriteId())) { return; + } else if (link && link->property(kFastShareProperty).value()) { + if (const auto item = _dragStateItem) { + const auto view = viewByItem(item); + const auto rightSize = view->rightActionSize().value_or(QSize()); + const auto reactionsSkip = view->embedReactionsInBubble() + ? 0 + : view->reactionButtonParameters({}, {}).reactionsHeight; + const auto top = itemTop(view) + + view->height() + - reactionsSkip + - _visibleAreaTop + - rightSize.height(); + const auto right = rect::right(view->innerGeometry()) + - st::historyFastShareLeft + - rightSize.width(); + HistoryView::ShowTopPeersSelector( + this, + _controller->uiShow(), + item->fullId(), + parentWidget()->mapToGlobal(QPoint(right, top))); + return; + } } auto selectedState = getSelectionState(); @@ -4772,6 +4795,12 @@ void HistoryInner::refreshAboutView(bool force) { _aboutView->refreshRequests() | rpl::on_next([=] { updateBotInfo(); }, _aboutView->lifetime()); + _aboutView->destroyRequests() | rpl::on_next([=] { + crl::on_main(this, [=] { + refreshAboutView(true); + update(); + }); + }, _aboutView->lifetime()); _aboutView->sendIntroSticker() | rpl::start_to_stream( _sendIntroSticker, _aboutView->lifetime()); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 81e5dd8321..034f780cb6 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2015,7 +2015,9 @@ void HistoryItem::destroy() { not_null HistoryItem::notificationThread() const { if (const auto rootId = topicRootId()) { if (const auto forum = _history->asForum()) { - return forum->enforceTopicFor(rootId); + if (!_history->peer->isBot()) { + return forum->enforceTopicFor(rootId); + } } } return _history; @@ -6563,6 +6565,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { : QString(), }, tr::marked); + } else if (action.is_craft()) { + result.text = tr::lng_action_gift_crafted( + tr::now, + tr::marked); } else { result.text = resale ? tr::lng_action_gift_self_bought( @@ -6788,6 +6794,36 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareNewCreatorPending = [this](const MTPDmessageActionNewCreatorPending &action) { + auto result = PreparedServiceText(); + auto user = _history->owner().user(action.vnew_creator_id().v); + result.links.push_back(fromLink()); + result.links.push_back(user->createOpenLink()); + result.text = tr::lng_action_new_creator_pending( + tr::now, + lt_user, + tr::link(user->name(), 2), + lt_from, + fromLinkText(), + tr::marked); + return result; + }; + + auto prepareChangeCreator = [this](const MTPDmessageActionChangeCreator &action) { + auto result = PreparedServiceText(); + auto user = _history->owner().user(action.vnew_creator_id().v); + result.links.push_back(fromLink()); + result.links.push_back(user->createOpenLink()); + result.text = tr::lng_action_change_creator( + tr::now, + lt_from, + fromLinkText(), + lt_user, + tr::link(user->name(), 2), + tr::marked); + return result; + }; + setServiceText(action.match( prepareChatAddUserText, prepareChatJoinedByLink, @@ -6847,6 +6883,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareSuggestBirthday, prepareStarGiftPurchaseOffer, prepareStarGiftPurchaseOfferDeclined, + prepareNewCreatorPending, + prepareChangeCreator, PrepareEmptyText, PrepareErrorText)); @@ -7108,6 +7146,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { .refunded = data.is_refunded(), .upgrade = data.is_upgrade(), .saved = data.is_saved(), + .craft = data.is_craft(), }; if (auto gift = Api::FromTL(&history()->session(), data.vgift())) { fields.stargiftId = gift->id; @@ -7123,6 +7162,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { unique->exportAt = data.vcan_export_at().value_or_empty(); unique->canTransferAt = data.vcan_transfer_at().value_or_empty(); unique->canResellAt = data.vcan_resell_at().value_or_empty(); + unique->canCraftAt = data.vcan_craft_at().value_or_empty(); } } _media = std::make_unique( diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 03750dca69..a5ec65bb12 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -776,6 +776,7 @@ ReplyKeyboard::ReplyKeyboard( , _st(std::move(s)) { if (const auto markup = _item->Get()) { const auto owner = &_item->history()->owner(); + const auto session = &owner->session(); const auto context = _item->fullId(); const auto rowCount = int(markup->data.rows.size()); _rows.reserve(rowCount); @@ -806,18 +807,24 @@ ReplyKeyboard::ReplyKeyboard( return withEmoji(st::chatSuggestDeclineIcon); } else if (type == Type::SuggestChange) { return withEmoji(st::chatSuggestChangeIcon); - } else if (type != Type::Buy) { - return TextWithEntities(); } auto result = TextWithEntities(); - auto firstPart = true; - for (const auto &part : text.split(QChar(0x2B50))) { - if (!firstPart) { - result.append(Ui::Text::IconEmoji( - &st::starIconEmojiLarge)); + if (const auto iconId = row[j].visual.iconId) { + using namespace Data; + result.append(SingleCustomEmoji(iconId)).append(' '); + } + if (type == Type::Buy) { + auto firstPart = true; + for (const auto &part : text.split(QChar(0x2B50))) { + if (!firstPart) { + result.append(Ui::Text::IconEmoji( + &st::starIconEmojiLarge)); + } + result.append(part); + firstPart = false; } - result.append(part); - firstPart = false; + } else if (!result.entities.empty()) { + result.append(text); } return result.entities.empty() ? TextWithEntities() @@ -833,7 +840,11 @@ ReplyKeyboard::ReplyKeyboard( button.text.setMarkedText( _st->textStyle(), TextUtilities::SingleLine(textWithEntities), - kMarkupTextOptions); + kMarkupTextOptions, + Core::TextContext({ + .session = session, + .repaint = [=] { _st->repaint(_item); }, + })); } else { button.text.setText( _st->textStyle(), @@ -841,6 +852,7 @@ ReplyKeyboard::ReplyKeyboard( kPlainTextOptions); } button.characters = text.isEmpty() ? 1 : text.size(); + button.color = row[j].visual.color; newRow.push_back(std::move(button)); } _rows.push_back(std::move(newRow)); @@ -855,7 +867,6 @@ void ReplyKeyboard::updateMessageId() { button.link->setMessageId(msgId); } } - } void ReplyKeyboard::resize(int width, int height) { @@ -975,11 +986,11 @@ void ReplyKeyboard::paint( const Ui::ChatStyle *st, Ui::BubbleRounding rounding, int outerWidth, - const QRect &clip) const { + const QRect &clip, + bool paused) const { Assert(_st != nullptr); Assert(_width > 0); - _st->startPaint(p, st); auto number = hasFastButtonMode() ? 1 : 0; for (auto y = 0, rowsCount = int(_rows.size()); y != rowsCount; ++y) { for (auto x = 0, count = int(_rows[y].size()); x != count; ++x) { @@ -1010,7 +1021,13 @@ void ReplyKeyboard::paint( && (rounding.bottomRight == Corner::Large)) ? Corner::Large : Corner::Small; - _st->paintButton(p, st, outerWidth, button, buttonRounding); + _st->paintButton( + p, + st, + outerWidth, + button, + buttonRounding, + paused); if (number) { p.setFont(st::dialogsUnreadFont); @@ -1202,11 +1219,16 @@ void ReplyKeyboard::Style::paintButton( const Ui::ChatStyle *st, int outerWidth, const ReplyKeyboard::Button &button, - Ui::BubbleRounding rounding) const { + Ui::BubbleRounding rounding, + bool paused) const { const auto &rect = button.rect; - paintButtonBg(p, st, rect, rounding, button.howMuchOver); + paintButtonBg(p, st, rect, button.color, rounding, button.howMuchOver); if (button.ripple) { - const auto color = st ? &st->msgBotKbRippleBg()->c : nullptr; + const auto color = st + ? &st->msgBotKbRippleBg()->c + : (button.color != HistoryMessageMarkupButton::Color::Normal) + ? &st::shadowFg->c + : nullptr; button.ripple->paint(p, rect.x(), rect.y(), outerWidth, color); if (button.ripple->empty()) { button.ripple.reset(); @@ -1218,7 +1240,13 @@ void ReplyKeyboard::Style::paintButton( || button.type == HistoryMessageMarkupButton::Type::Game) { if (const auto data = button.link->getButton()) { if (data->requestId) { - paintButtonLoading(p, st, rect, outerWidth, rounding); + paintButtonLoading( + p, + st, + rect, + button.color, + outerWidth, + rounding); } } } @@ -1231,13 +1259,17 @@ void ReplyKeyboard::Style::paintButton( tx += (tw - st::botKbStyle.font->elidew) / 2; tw = st::botKbStyle.font->elidew; } - button.text.drawElided( - p, - tx, - rect.y() + _st->textTop + ((rect.height() - _st->height) / 2), - tw, - 1, - style::al_top); + paintButtonStart(p, st, button.color); + button.text.draw(p, { + .position = { + tx, + rect.y() + _st->textTop + ((rect.height() - _st->height) / 2), + }, + .availableWidth = tw, + .align = style::al_top, + .paused = paused || On(PowerSaving::kEmojiChat), + .elisionLines = 1, + }); if (button.type == HistoryMessageMarkupButton::Type::SimpleWebView) { const auto &icon = st::markupWebview; st::markupWebview.paint( @@ -1289,6 +1321,7 @@ void HistoryMessageReplyMarkup::updateSuggestControls( | ReplyMarkupFlag::SuggestionDecline; } using Type = HistoryMessageMarkupButton::Type; + using Visual = HistoryMessageMarkupButton::Visual; const auto has = [&](Type type) { return !data.rows.empty() && ranges::contains( @@ -1304,10 +1337,12 @@ void HistoryMessageReplyMarkup::updateSuggestControls( { Type::SuggestDecline, tr::lng_action_gift_offer_decline(tr::now), + Visual(), }, { Type::SuggestAccept, tr::lng_action_gift_offer_accept(tr::now), + Visual(), }, }); } else if (actions == SuggestionActions::AcceptAndDecline) { @@ -1324,15 +1359,18 @@ void HistoryMessageReplyMarkup::updateSuggestControls( { Type::SuggestDecline, tr::lng_suggest_action_decline(tr::now), + Visual(), }, { Type::SuggestAccept, tr::lng_suggest_action_accept(tr::now), + Visual(), }, }); data.rows.push_back({ { Type::SuggestChange, tr::lng_suggest_action_change(tr::now), + Visual(), } }); data.flags |= ReplyMarkupFlag::SuggestionAccept | ReplyMarkupFlag::SuggestionDecline; @@ -1364,6 +1402,7 @@ void HistoryMessageReplyMarkup::updateSuggestControls( data.rows.push_back({ { Type::SuggestDecline, tr::lng_suggest_action_decline(tr::now), + Visual(), } }); data.flags |= ReplyMarkupFlag::SuggestionDecline; } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 095602c604..4267e928fb 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -457,9 +457,6 @@ public: Style(const style::BotKeyboardButton &st) : _st(&st) { } - virtual void startPaint( - QPainter &p, - const Ui::ChatStyle *st) const = 0; virtual const style::TextStyle &textStyle() const = 0; int buttonSkip() const; @@ -478,8 +475,13 @@ public: QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, Ui::BubbleRounding rounding, float64 howMuchOver) const = 0; + virtual void paintButtonStart( + QPainter &p, + const Ui::ChatStyle *st, + HistoryMessageMarkupButton::Color color) const = 0; virtual void paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, @@ -490,6 +492,7 @@ public: QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, int outerWidth, Ui::BubbleRounding rounding) const = 0; virtual int minButtonWidth( @@ -503,7 +506,8 @@ public: const Ui::ChatStyle *st, int outerWidth, const ReplyKeyboard::Button &button, - Ui::BubbleRounding rounding) const; + Ui::BubbleRounding rounding, + bool paused) const; friend class ReplyKeyboard; }; @@ -527,7 +531,8 @@ public: const Ui::ChatStyle *st, Ui::BubbleRounding rounding, int outerWidth, - const QRect &clip) const; + const QRect &clip, + bool paused) const; ClickHandlerPtr getLink(QPoint point) const; ClickHandlerPtr getLinkByIndex(int index) const; @@ -554,12 +559,14 @@ private: QRect rect; int characters = 0; float64 howMuchOver = 0.; - HistoryMessageMarkupButton::Type type; + HistoryMessageMarkupButton::Type type = {}; + HistoryMessageMarkupButton::Color color = {}; std::shared_ptr link; mutable std::unique_ptr ripple; }; struct ButtonCoords { - int i, j; + int i = 0; + int j = 0; }; void startAnimation(int i, int j, int direction); diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index b769e7a19d..f4c02da494 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -14,6 +14,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +[[nodiscard]] HistoryMessageMarkupButton::Visual ParseVisual( + const tl::conditional &style) { + if (!style) { + return {}; + } + using Color = HistoryMessageMarkupButton::Color; + const auto &data = style->data(); + if (data.vicon()) { + [[maybe_unused]] int a = 0; + } + return { + .iconId = data.vicon().value_or_empty(), + .color = (data.is_bg_danger() + ? Color::Danger + : data.is_bg_primary() + ? Color::Primary + : data.is_bg_success() + ? Color::Success + : Color::Normal), + }; +} + [[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( const MTPDkeyboardButtonRequestPeer &query) { using Type = RequestPeerQuery::Type; @@ -79,10 +101,12 @@ InlineBots::PeerTypes PeerTypesFromMTP( HistoryMessageMarkupButton::HistoryMessageMarkupButton( Type type, const QString &text, + Visual visual, const QByteArray &data, const QString &forwardText, int64 buttonId) : type(type) +, visual(visual) , text(text) , forwardText(forwardText) , data(data) @@ -122,23 +146,34 @@ void HistoryMessageMarkupData::fillRows( row.reserve(data.vbuttons().v.size()); for (const auto &button : data.vbuttons().v) { button.match([&](const MTPDkeyboardButton &data) { - row.emplace_back(Type::Default, qs(data.vtext())); + row.emplace_back( + Type::Default, + qs(data.vtext()), + ParseVisual(data.vstyle())); }, [&](const MTPDkeyboardButtonCallback &data) { row.emplace_back( (data.is_requires_password() ? Type::CallbackWithPassword : Type::Callback), qs(data.vtext()), + ParseVisual(data.vstyle()), qba(data.vdata())); }, [&](const MTPDkeyboardButtonRequestGeoLocation &data) { - row.emplace_back(Type::RequestLocation, qs(data.vtext())); + row.emplace_back( + Type::RequestLocation, + qs(data.vtext()), + ParseVisual(data.vstyle())); }, [&](const MTPDkeyboardButtonRequestPhone &data) { - row.emplace_back(Type::RequestPhone, qs(data.vtext())); + row.emplace_back( + Type::RequestPhone, + qs(data.vtext()), + ParseVisual(data.vstyle())); }, [&](const MTPDkeyboardButtonRequestPeer &data) { const auto query = RequestPeerQueryFromTL(data); row.emplace_back( Type::RequestPeer, qs(data.vtext()), + ParseVisual(data.vstyle()), QByteArray( reinterpret_cast(&query), sizeof(query)), @@ -148,12 +183,17 @@ void HistoryMessageMarkupData::fillRows( row.emplace_back( Type::Url, qs(data.vtext()), + ParseVisual(data.vstyle()), qba(data.vurl())); }, [&](const MTPDkeyboardButtonSwitchInline &data) { const auto type = data.is_same_peer() ? Type::SwitchInlineSame : Type::SwitchInline; - row.emplace_back(type, qs(data.vtext()), qba(data.vquery())); + row.emplace_back( + type, + qs(data.vtext()), + ParseVisual(data.vstyle()), + qba(data.vquery())); if (type == Type::SwitchInline) { // Optimization flag. // Fast check on all new messages if there is a switch button to auto-click it. @@ -163,13 +203,20 @@ void HistoryMessageMarkupData::fillRows( } } }, [&](const MTPDkeyboardButtonGame &data) { - row.emplace_back(Type::Game, qs(data.vtext())); + row.emplace_back( + Type::Game, + qs(data.vtext()), + ParseVisual(data.vstyle())); }, [&](const MTPDkeyboardButtonBuy &data) { - row.emplace_back(Type::Buy, qs(data.vtext())); + row.emplace_back( + Type::Buy, + qs(data.vtext()), + ParseVisual(data.vstyle())); }, [&](const MTPDkeyboardButtonUrlAuth &data) { row.emplace_back( Type::Auth, qs(data.vtext()), + ParseVisual(data.vstyle()), qba(data.vurl()), qs(data.vfwd_text().value_or_empty()), data.vbutton_id().v); @@ -187,11 +234,13 @@ void HistoryMessageMarkupData::fillRows( row.emplace_back( Type::RequestPoll, qs(data.vtext()), + ParseVisual(data.vstyle()), quiz); }, [&](const MTPDkeyboardButtonUserProfile &data) { row.emplace_back( Type::UserProfile, qs(data.vtext()), + ParseVisual(data.vstyle()), QByteArray::number(data.vuser_id().v)); }, [&](const MTPDinputKeyboardButtonUrlAuth &data) { LOG(("API Error: inputKeyboardButtonUrlAuth.")); @@ -203,16 +252,19 @@ void HistoryMessageMarkupData::fillRows( row.emplace_back( Type::WebView, qs(data.vtext()), + ParseVisual(data.vstyle()), data.vurl().v); }, [&](const MTPDkeyboardButtonSimpleWebView &data) { row.emplace_back( Type::SimpleWebView, qs(data.vtext()), + ParseVisual(data.vstyle()), data.vurl().v); }, [&](const MTPDkeyboardButtonCopy &data) { row.emplace_back( Type::CopyText, qs(data.vtext()), + ParseVisual(data.vstyle()), data.vcopy_text().v); }, [&](const MTPDinputKeyboardButtonRequestPeer &data) { LOG(("API Error: inputKeyboardButtonRequestPeer.")); @@ -283,6 +335,7 @@ void HistoryMessageMarkupData::fillForwardedData( row.emplace_back( newType, text, + button.visual, button.data, QString(), button.buttonId); diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index f180606a35..2614b20c87 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -70,7 +70,7 @@ struct RequestPeerQuery { static_assert(std::is_trivially_copy_assignable_v); struct HistoryMessageMarkupButton { - enum class Type { + enum class Type : uchar { Default, Url, Callback, @@ -94,9 +94,22 @@ struct HistoryMessageMarkupButton { SuggestChange, }; + enum class Color : uchar { + Normal, + Primary, + Danger, + Success, + }; + + struct Visual { + DocumentId iconId = 0; + Color color = Color::Normal; + }; + HistoryMessageMarkupButton( Type type, const QString &text, + Visual visual, const QByteArray &data = QByteArray(), const QString &forwardText = QString(), int64 buttonId = 0); @@ -108,6 +121,7 @@ struct HistoryMessageMarkupButton { int column); Type type; + Visual visual; QString text, forwardText; QByteArray data; int64 buttonId = 0; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index fe104281e7..fad0b286bd 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -432,11 +432,21 @@ HistoryWidget::HistoryWidget( }, _field->lifetime()); _field->cancelled( ) | rpl::on_next([=] { + if (_peer->amMonoforumAdmin()) { + QWidget::setEnabled(false); + crl::on_main([=] { + QWidget::setEnabled(true); + QWidget::setFocus(); + }); + } escape(); }, _field->lifetime()); _field->tabbed( - ) | rpl::on_next([=] { - fieldTabbed(); + ) | rpl::on_next([=](not_null handled) { + if (_supportAutocomplete) { + _supportAutocomplete->activate(_field.data()); + *handled = true; + } }, _field->lifetime()); _field->heightChanges( ) | rpl::on_next([=] { @@ -1150,6 +1160,14 @@ HistoryWidget::HistoryWidget( setupSendAsToggle(); orderWidgets(); setupShortcuts(); + + _attachToggle->setAccessibleName(tr::lng_attach(tr::now)); + _tabbedSelectorToggle->setAccessibleName(tr::lng_emoji_sticker_gif(tr::now)); + _botKeyboardShow->setAccessibleName(tr::lng_bot_keyboard_show(tr::now)); + _botKeyboardHide->setAccessibleName(tr::lng_bot_keyboard_hide(tr::now)); + _botCommandStart->setAccessibleName(tr::lng_bot_commands_start(tr::now)); + _fieldBarCancel->setAccessibleName(tr::lng_cancel(tr::now)); + } void HistoryWidget::setGeometryWithTopMoved( @@ -2292,6 +2310,7 @@ void HistoryWidget::setupGiftToChannelButton() { _giftToChannel = Ui::CreateChild( _muteUnmute.data(), st::historyGiftToChannel); + _giftToChannel->setAccessibleName(tr::lng_gift_channel_title(tr::now)); widthValue() | rpl::on_next([=](int width) { _giftToChannel->moveToRight(0, 0, width); }, _giftToChannel->lifetime()); @@ -2319,6 +2338,7 @@ void HistoryWidget::setupDirectMessageButton() { _directMessage = Ui::CreateChild( _muteUnmute.data(), st::historyDirectMessage); + _directMessage->setAccessibleName(tr::lng_profile_direct_messages(tr::now)); widthValue() | rpl::on_next([=](int width) { _directMessage->moveToLeft(0, 0, width); }, _directMessage->lifetime()); @@ -2711,6 +2731,7 @@ void HistoryWidget::showHistory( updateReplaceMediaButton(); _fieldBarCancel->hide(); + _mediaEditManager.cancel(); _membersDropdownShowTimer.cancel(); _scroll->takeWidget().destroy(); @@ -3218,6 +3239,9 @@ bool HistoryWidget::updateReplaceMediaButton() { _replaceMedia.create( this, _canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia); + _replaceMedia->setAccessibleName(_canReplaceMedia + ? tr::lng_attach_replace(tr::now) + : tr::lng_attach(tr::now)); const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration; _replaceMedia->setClickedCallback([=] { base::call_delayed(hideDuration, this, [=] { @@ -3329,6 +3353,7 @@ void HistoryWidget::refreshScheduledToggle() { && (session().scheduledMessages().count(_history) > 0); if (!_scheduled && has) { _scheduled.create(this, st::historyScheduledToggle); + _scheduled->setAccessibleName(tr::lng_scheduled_messages(tr::now)); _scheduled->show(); _scheduled->addClickHandler([=] { controller()->showSection( @@ -3359,6 +3384,7 @@ void HistoryWidget::refreshSendGiftToggle() { && ((disallowed & all) != all); if (!_giftToUser && has) { _giftToUser.create(this, st::historyGiftToUser); + _giftToUser->setAccessibleName(tr::lng_gift_send_title(tr::now)); _giftToUser->show(); _giftToUser->addClickHandler([=] { Ui::ShowStarGiftBox(controller(), _peer); @@ -3441,6 +3467,7 @@ void HistoryWidget::refreshSendAsToggle() { } const auto &st = st::defaultComposeControls.chooseSendAs; _sendAs.create(this, st.button); + _sendAs->setAccessibleName(tr::lng_send_as_title(tr::now)); Ui::SetupSendAsButton(_sendAs.data(), st, controller()); } @@ -4835,17 +4862,31 @@ Api::SendAction HistoryWidget::prepareSendAction( result.replyTo = replyTo(); if (const auto forum = _history->asForum()) { - if (_history->peer->isBot()) { - if (!_creatingBotTopic) { - const auto &colors = Data::ForumTopicColorIds(); - const auto colorId = colors[base::RandomIndex(colors.size())]; - _creatingBotTopic = forum->topicFor(forum->reserveCreatingId( - tr::lng_bot_new_chat(tr::now), - colorId, - DocumentId())); + if (Data::IsBotCanManageTopics(_history->peer)) { + const auto readyRootId = [&]() -> MsgId { + if (const auto id = result.replyTo.messageId) { + if (const auto item = session().data().message(id)) { + return item->topicRootId(); + } + } + return {}; + }(); + if (readyRootId) { + result.replyTo.topicRootId = readyRootId; + } else { + if (!_creatingBotTopic) { + const auto &colors = Data::ForumTopicColorIds(); + const auto colorId + = colors[base::RandomIndex(colors.size())]; + _creatingBotTopic = forum->topicFor( + forum->reserveCreatingId( + tr::lng_bot_new_chat(tr::now), + colorId, + DocumentId())); + } + result = Api::SendAction(_creatingBotTopic, options); + result.replyTo.topicRootId = _creatingBotTopic->rootId(); } - result = Api::SendAction(_creatingBotTopic, options); - result.replyTo.topicRootId = _creatingBotTopic->rootId(); } } @@ -5884,7 +5925,9 @@ bool HistoryWidget::searchInChatEmbedded( Dialogs::Key chat, PeerData *searchFrom) { const auto peer = chat.peer(); // windows todo - if (!peer || Window::SeparateId(peer) != controller()->windowId()) { + if (!peer + || ((Window::SeparateId(peer) != controller()->windowId()) + && !controller()->isPrimary())) { return false; } else if (_peer != peer) { const auto weak = base::make_weak(this); @@ -6438,6 +6481,13 @@ void HistoryWidget::updateFieldPlaceholder() { } else { return tr::lng_message_ph(); } + } else if (const auto user = peer->asUser()) { + if (const auto &info = user->botInfo) { + if (info->forum() && !info->canManageTopics) { + return tr::lng_bot_off_thread_ph(); + } + } + return tr::lng_message_ph(); } else { return tr::lng_message_ph(); } @@ -6731,7 +6781,11 @@ bool HistoryWidget::confirmSendingFiles( std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel) { if (!canWriteMessage()) { - return false; + if (_composeSearch) { + _composeSearch->hideAnimated(); + } else { + return false; + } } const auto hasImage = data->hasImage(); @@ -7923,9 +7977,10 @@ bool HistoryWidget::replyToPreviousMessage() { || (_replyTo && _replyTo.messageId.peer != _history->peer->id)) { return false; } + const auto isFieldVisible = _field->isVisible(); const auto fullId = FullMsgId( _history->peer->id, - (_field->isVisible() + ((isFieldVisible && _replyTo.messageId.msg) ? _replyTo.messageId.msg : _highlighter.latestSingleHighlightedMsgId())); if (const auto item = session().data().message(fullId)) { @@ -7933,8 +7988,12 @@ bool HistoryWidget::replyToPreviousMessage() { if (const auto previousView = view->previousDisplayedInBlocks()) { const auto previous = previousView->data(); controller()->showMessage(previous); - if (_field->isVisible()) { - replyToMessage(previous); + if (isFieldVisible) { + if (previous->isLocal()) { + cancelReply(false, true); + } else { + replyToMessage(previous); + } } return true; } @@ -7942,8 +8001,12 @@ bool HistoryWidget::replyToPreviousMessage() { } else if (const auto previousView = _history->findLastDisplayed()) { const auto previous = previousView->data(); controller()->showMessage(previous); - if (_field->isVisible()) { - replyToMessage(previous); + if (isFieldVisible) { + if (previous->isLocal()) { + cancelReply(); + } else { + replyToMessage(previous); + } } return true; } @@ -7957,9 +8020,10 @@ bool HistoryWidget::replyToNextMessage() { || (_replyTo && _replyTo.messageId.peer != _history->peer->id)) { return false; } + const auto isFieldVisible = _field->isVisible(); const auto fullId = FullMsgId( _history->peer->id, - (_field->isVisible() + ((isFieldVisible && _replyTo.messageId.msg) ? _replyTo.messageId.msg : _highlighter.latestSingleHighlightedMsgId())); if (const auto item = session().data().message(fullId)) { @@ -7967,11 +8031,14 @@ bool HistoryWidget::replyToNextMessage() { if (const auto nextView = view->nextDisplayedInBlocks()) { const auto next = nextView->data(); controller()->showMessage(next); - if (_field->isVisible()) { - replyToMessage(next); + if (isFieldVisible) { + if (next->isLocal()) { + cancelReply(false, true); + } else { + replyToMessage(next); + } } } else { - _highlighter.clear(); cancelReply(false); } return true; @@ -8005,12 +8072,6 @@ bool HistoryWidget::showSlowmodeError() { return true; } -void HistoryWidget::fieldTabbed() { - if (_supportAutocomplete) { - _supportAutocomplete->activate(_field.data()); - } -} - void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { if (!_peer || !_canSendMessages) { return; @@ -8940,7 +9001,9 @@ void HistoryWidget::processReply() { } else if (const auto forum = _peer->forum() ; forum && _processingReplyItem->history() == _history) { const auto topicRootId = _processingReplyItem->topicRootId(); - if (forum->topicDeleted(topicRootId)) { + using namespace Data; + if (forum->topicDeleted(topicRootId) + && !(topicRootId == ForumTopic::kGeneralId && _peer->isBot())) { return processCancel(); } else if (const auto topic = forum->topicFor(topicRootId)) { if (!Data::CanSendAnything(topic)) { @@ -8981,6 +9044,19 @@ void HistoryWidget::setReplyFieldsFromProcessing() { } else { _replyEditMsg = item; _replyTo = id; + if (_replyTo) { + if (const auto i = session().data().message(_replyTo.messageId)) { + if (const auto media = i->media()) { + using namespace SendMenu; + const auto type = media->hasSpoiler() + ? Action{ .type = Action::Type::SpoilerOn } + : Action{ .type = Action::Type::SpoilerOff }; + _mediaEditManager.apply(type); + } + } + } else { + _mediaEditManager.cancel(); + } cancelSuggestPost(); updateReplyEditText(_replyEditMsg); updateCanSendMessage(); @@ -9009,7 +9085,8 @@ void HistoryWidget::editMessage( Window::PeerMenuEditTodoList(controller(), item); return; } - } else if (_composeSearch) { + } + if (_composeSearch) { _composeSearch->hideAnimated(); } @@ -9124,7 +9201,9 @@ bool HistoryWidget::cancelReplyOrSuggest(bool lastKeyboardUsed) { return ok1 || ok2; } -bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { +bool HistoryWidget::cancelReply( + bool lastKeyboardUsed, + bool keepHighlighterState) { bool wasReply = false; if (_replyTo) { wasReply = true; @@ -9166,6 +9245,9 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { toggleKeyboard(false); } } + if (!keepHighlighterState) { + _highlighter.clear(); + } return wasReply; } @@ -9283,6 +9365,11 @@ void HistoryWidget::fullInfoUpdated() { } else if (!_scroll->isHidden() && _unblock->isHidden() == isBlocked()) { refresh = true; } + if (_history + && HistoryView::SubsectionTabs::UsedFor(_history) + && !_subsectionTabs) { + validateSubsectionTabs(); + } if (refresh) { updateControlsVisibility(); updateControlsGeometry(); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 27b0ef4246..b6ec3d7c43 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -222,7 +222,9 @@ public: bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; bool cancelReplyOrSuggest(bool lastKeyboardUsed = false); - bool cancelReply(bool lastKeyboardUsed = false); + bool cancelReply( + bool lastKeyboardUsed = false, + bool keepHighlighterState = false); bool cancelSuggestPost(); void cancelEdit(); void updateForwarding(); @@ -387,7 +389,6 @@ private: void setTabbedPanel(std::unique_ptr panel); void updateField(); void fieldChanged(); - void fieldTabbed(); void fieldFocused(); void fieldResized(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 23329e5ab5..b4aea09682 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -76,7 +76,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" #include "menu/menu_send.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/item_text_options.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp index 2897317dff..66659ad75a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp @@ -60,6 +60,8 @@ void MediaEditManager::apply(SendMenu::Action action) { void MediaEditManager::cancel() { _menu = nullptr; _item = nullptr; + _spoilered = false; + _invertCaption = false; _lifetime.destroy(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index a363e11c72..6593982d10 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -899,11 +899,17 @@ void DraftOptionsBox( const auto small = state->webpage.forceSmallMedia || (!state->webpage.forceLargeMedia && state->preview->computeDefaultSmallMedia()); + const auto hasVideo = state->preview->document + && state->preview->document->isVideoFile(); Settings::AddButtonWithIcon( bottom, (small - ? tr::lng_link_enlarge_photo() - : tr::lng_link_shrink_photo()), + ? (hasVideo + ? tr::lng_link_enlarge_video() + : tr::lng_link_enlarge_photo()) + : (hasVideo + ? tr::lng_link_shrink_video() + : tr::lng_link_shrink_photo())), st::settingsButton, { small ? &st::menuIconEnlarge : &st::menuIconShrink } )->setClickedCallback([=] { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index a93373f96d..4501944aa8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -504,6 +504,9 @@ TTLButton::TTLButton( } }, lifetime()); + setAccessibleName((recordingVideo + ? tr::lng_in_dlg_video_message_ttl + : tr::lng_in_dlg_voice_message_ttl)(tr::now)); } void TTLButton::clearState() { @@ -612,6 +615,7 @@ ListenWrap::ListenWrap( , _inactiveWaveformBar( anim::with_alpha(_activeWaveformBar, kInactiveWaveformBarAlpha)) , _playPause(_playPauseSt, [=] { _playPauseButton->update(); }) { + _delete->setAccessibleName(tr::lng_record_lock_delete(tr::now)); init(); } @@ -760,6 +764,7 @@ void ListenWrap::initPlayButton() { const auto &width = _waveformBgFinalCenterRect.height(); _playPauseButton->resize(width, width); _playPauseButton->show(); + _playPauseButton->setAccessibleName(tr::lng_record_lock_play(tr::now)); _playPauseButton->paintRequest( ) | rpl::on_next([=](const QRect &clip) { @@ -781,6 +786,9 @@ void ListenWrap::initPlayButton() { const auto showPause = _lifetime.make_state>(false); showPause->changes( ) | rpl::on_next([=](bool pause) { + _playPauseButton->setAccessibleName(pause + ? tr::lng_record_lock_pause(tr::now) + : tr::lng_record_lock_play(tr::now)); _playPause.setState(pause ? PlayButtonLayout::State::Pause : PlayButtonLayout::State::Play); @@ -1062,6 +1070,7 @@ void RecordLock::init() { } drawProgress(p); }, lifetime()); + setAccessibleName(tr::lng_record_lock(tr::now)); } void RecordLock::drawProgress(QPainter &p) { @@ -1308,6 +1317,7 @@ CancelButton::CancelButton( , _width(st::historyRecordCancelButtonWidth) , _rippleRect(QRect(0, (height - _width) / 2, _width, _width)) , _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now)) { + setAccessibleName(tr::lng_record_cancel_recording(tr::now)); resize(_width, height); init(); } @@ -1625,6 +1635,9 @@ void VoiceRecordBar::init() { _paused.value() | rpl::distinct_until_changed( ) | rpl::on_next([=](bool paused) { + _lock->setAccessibleName(paused + ? tr::lng_record_lock_resume(tr::now) + : tr::lng_record_lock(tr::now)); if (!paused) { return; } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index b796d27858..dcbec6690a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -7,11 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/controls/history_view_voice_record_button.h" +#include "lottie/lottie_icon.h" #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" +#include "lang/lang_keys.h" #include @@ -24,6 +26,8 @@ constexpr auto kBlobAlpha = 76. / 255.; constexpr auto kBlobMaxSpeed = 5.0; constexpr auto kLevelDuration = 100. + 500. * 0.33; constexpr auto kBlobsScaleEnterDuration = crl::time(250); +constexpr auto kVoiceIconIndex = 0; +constexpr auto kRoundIconIndex = 1; auto Blobs() { return std::vector{ @@ -135,22 +139,28 @@ void VoiceRecordButton::init() { p.scale(scale, scale); } const auto state = *currentState; - const auto icon = (state == Type::Send) - ? st::historySendIcon - : (state == Type::Record) - ? st::historyRecordVoiceActive - : st::historyRecordRoundActive; - const auto position = (state == Type::Send) - ? st::historyRecordSendIconPosition - : (state == Type::Record) - ? QPoint(0, 0) - : st::historyRecordRoundIconPosition; - icon.paint( - p, - -icon.width() / 2 + position.x(), - -icon.height() / 2 + position.y(), - 0, - st::historyRecordVoiceFgActiveIcon->c); + if (state == Type::Send) { + const auto icon = st::historySendIcon; + const auto position = st::historyRecordSendIconPosition; + icon.paint( + p, + -icon.width() / 2 + position.x(), + -icon.height() / 2 + position.y(), + 0, + st::historyRecordVoiceFgActiveIcon->c); + } else { + const auto index = (state == Type::Record) + ? kVoiceIconIndex + : kRoundIconIndex; + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } + icon->paintInCenter( + p, + rect().translated(-_center, -_center), + st::historyRecordVoiceFgActiveIcon->c); + } } }, lifetime()); @@ -205,6 +215,18 @@ void VoiceRecordButton::init() { }, lifetime()); } +void VoiceRecordButton::initVoiceRoundIcon(int index) { + Expects(index >= 0 && index < 2); + + _voiceRoundIcons[index] = Lottie::MakeIcon({ + .path = ((index == kVoiceIconIndex) + ? u":/animations/chat/voice_to_video.tgs"_q + : u":/animations/chat/video_to_voice.tgs"_q), + .sizeOverride = st::historySend.recordSize, + .colorizeUsingAlpha = true, + }); +} + rpl::producer VoiceRecordButton::actives() const { return events( ) | rpl::filter([=](not_null e) { @@ -259,6 +281,19 @@ void VoiceRecordButton::requestPaintColor(float64 progress) { void VoiceRecordButton::setType(Type state) { _state = state; + + setAccessibleName([&] { + switch (state) { + case Type::Send: + return tr::lng_send_button(tr::now); + case Type::Record: + return tr::lng_send_action_record_round(tr::now); + case Type::Round: + return tr::lng_send_action_record_round(tr::now); + } + Unexpected("Voice record button type."); + }()); + } } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index e8e2dd7b65..4465a95925 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -15,6 +15,10 @@ namespace style { struct RecordBar; } // namespace style +namespace Lottie { +class Icon; +} // namespace Lottie + namespace Ui::Paint { class Blobs; } // namespace Ui::Paint @@ -47,8 +51,10 @@ public: private: void init(); + void initVoiceRoundIcon(int index); std::unique_ptr _blobs; + std::array, 2> _voiceRoundIcons; crl::time _lastUpdateTime = 0; crl::time _blobsHideLastTime = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 1ee5a2ae65..692abd8b15 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -37,8 +37,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_chat_intro.h" -#include "settings/settings_credits.h" // BuyStarsHandler -#include "settings/settings_premium.h" +#include "settings/sections/settings_credits.h" // BuyStarsHandler +#include "settings/sections/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/text/custom_emoji_instance.h" #include "ui/text/text_utilities.h" @@ -155,18 +155,6 @@ private: }; -class NewBotThreadDownIcon final : public MediaGenericPart { -public: - void draw( - Painter &p, - not_null owner, - const PaintContext &context, - int outerWidth) const override; - QSize countOptimalSize() override; - QSize countCurrentSize(int newWidth) override; - -}; - UserpicsList::UserpicsList( std::vector> peers, const style::GroupCallUserpics &st, @@ -227,26 +215,7 @@ int UserpicsList::width() const { return _st.size + (shifted * (_st.size - _st.shift)); } -void NewBotThreadDownIcon::draw( - Painter &p, - not_null owner, - const PaintContext &context, - int outerWidth) const { - auto color = context.st->msgServiceFg()->c; - color.setAlphaF(color.alphaF() * kLabelOpacity); - st::newBotThreadDown.paintInCenter( - p, - QRect(0, 0, outerWidth, st::newBotThreadDown.height()), - color); -} -QSize NewBotThreadDownIcon::countOptimalSize() { - return st::newBotThreadDown.size(); -} - -QSize NewBotThreadDownIcon::countCurrentSize(int newWidth) { - return st::newBotThreadDown.size(); -} NewBotThreadDottedLine::NewBotThreadDottedLine(not_null parent) : _parent(parent) { @@ -366,9 +335,46 @@ auto GenerateNewBotThread( const auto title = tr::lng_bot_new_thread_title(tr::now); const auto description = tr::lng_bot_new_thread_about(tr::now); push(std::make_unique(parent)); + push(std::make_unique( + QSize( + st::newThreadAboutIconOuter, + st::newThreadAboutIconOuter + st::newThreadAboutIconSkip), + [=]( + Painter &p, + not_null owner, + const PaintContext &context, + int outerWidth) { + const auto size = st::newThreadAboutIconOuter; + const auto &icon = st::newThreadAboutIcon; + const auto x = (outerWidth - icon.width()) / 2; + const auto y = (size - icon.height()) / 2 + + st::newThreadAboutIconSkip; + p.setPen(Qt::NoPen); + p.setBrush(context.st->msgServiceBgSelected()); + p.drawEllipse( + (outerWidth - size) / 2, + st::newThreadAboutIconSkip, + size, + size); + const auto color = context.st->msgServiceFg(); + icon.paint(p, x, y, outerWidth, color->c); + })); pushText(tr::bold(title), st::chatIntroTitleMargin); pushText({ description }, st::chatIntroMargin); - push(std::make_unique()); + push(std::make_unique( + st::newBotThreadDown.size() / 4 * 3, + [=, h = st::newBotThreadDown.height() / 2 + st::lineWidth * 4]( + Painter &p, + not_null owner, + const PaintContext &context, + int outerWidth) { + auto color = context.st->msgServiceFg()->c; + color.setAlphaF(color.alphaF() * kLabelOpacity); + st::newBotThreadDown.paintInCenter( + p, + QRect(0, 0, outerWidth, h), + color); + })); parent->addVerticalMargins( st::newBotThreadTopSkip - st::msgServiceMargin.top(), @@ -626,7 +632,13 @@ HistoryItem *AboutView::item() const { } bool AboutView::aboveHistory() const { - return !_history->peer->isBot() || !_history->isForum(); + if (!_history->peer->isBot() || !_history->isForum()) { + return true; + } + const auto info = _history->peer->asUser()->botInfo.get(); + return !(info->canManageTopics + && info->startToken.isEmpty() + && (!_history->isEmpty() || _history->lastMessage())); } bool AboutView::refresh() { @@ -683,7 +695,10 @@ bool AboutView::refresh() { } _version = 0; return false; - } else if (_history->peer->isForum()) { + } else if (_history->peer->isForum() + && info->canManageTopics + && info->startToken.isEmpty() + && (!_history->isEmpty() || _history->lastMessage())) { if (_item) { return false; } @@ -695,6 +710,14 @@ bool AboutView::refresh() { return false; } _version = version; + if (_history->peer->isBot() && _history->peer->isForum()) { + _history->session().data().newItemAdded( + ) | rpl::on_next([=](not_null item) { + if (item->history() == _history) { + _destroyRequests.fire({}); + } + }, lifetime()); + } setItem(makeAboutBot(info), nullptr); return true; } @@ -716,7 +739,7 @@ void AboutView::make(Data::ChatIntro data, bool preview) { | MessageFlag::FakeHistoryItem | MessageFlag::Local), .from = _history->peer->id, - }, PreparedServiceText{ { text }}); + }, PreparedServiceText{ { text } }); if (data.sticker) { _helloChosen = nullptr; @@ -772,6 +795,10 @@ rpl::producer<> AboutView::refreshRequests() const { return _refreshRequests.events(); } +rpl::producer<> AboutView::destroyRequests() const { + return _destroyRequests.events(); +} + rpl::lifetime &AboutView::lifetime() { return _lifetime; } @@ -885,7 +912,7 @@ AdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null user) { | MessageFlag::FakeHistoryItem | MessageFlag::Local), .from = _history->peer->id, - }, PreparedServiceText{ { text }}); + }, PreparedServiceText{ { text } }); auto owned = AdminLog::OwnedItem(_delegate, item); owned->overrideMedia(std::make_unique( @@ -1028,7 +1055,7 @@ AdminLog::OwnedItem AboutView::makeNewBotThread() { result.get(), GenerateNewBotThread(result.get(), _item.get()), HistoryView::MediaGenericDescriptor{ - .maxWidth = st::chatIntroWidth, + .maxWidth = st::newThreadAboutMaxWidth, .service = true, .hideServiceText = true, })); diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h index 35f5bd85f5..e9ddfb2fae 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.h +++ b/Telegram/SourceFiles/history/view/history_view_about_view.h @@ -34,6 +34,7 @@ public: [[nodiscard]] auto sendIntroSticker() const -> rpl::producer>; [[nodiscard]] rpl::producer<> refreshRequests() const; + [[nodiscard]] rpl::producer<> destroyRequests() const; [[nodiscard]] rpl::lifetime &lifetime(); int top = 0; @@ -73,6 +74,7 @@ private: bool _commonGroupsRequested = false; std::vector> _commonGroups; rpl::event_stream<> _refreshRequests; + rpl::event_stream<> _destroyRequests; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 7f4463c435..a9fccef31b 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -37,7 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_peer_values.h" #include "data/stickers/data_custom_emoji.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/window_peer_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index f3e17d6f26..ac2cb51f43 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -79,7 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "base/platform/base_platform_info.h" #include "base/call_delayed.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/window_peer_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 15fe7d63b8..c5ef6d0920 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -87,9 +87,6 @@ public: Ui::BubbleRounding outer, RectParts sides) const override; - void startPaint( - QPainter &p, - const Ui::ChatStyle *st) const override; const style::TextStyle &textStyle() const override; void repaint(not_null item) const override; @@ -98,8 +95,13 @@ protected: QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, Ui::BubbleRounding rounding, float64 howMuchOver) const override; + void paintButtonStart( + QPainter &p, + const Ui::ChatStyle *st, + HistoryMessageMarkupButton::Color color) const override; void paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, @@ -110,6 +112,7 @@ protected: QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, int outerWidth, Ui::BubbleRounding rounding) const override; int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; @@ -120,8 +123,19 @@ private: QColor color; }; using BubbleRoundingKey = uchar; - mutable base::flat_map _cachedBg; - mutable base::flat_map _cachedOutline; + struct CacheKey { + BubbleRoundingKey rounding; + HistoryMessageMarkupButton::Color color; + + friend inline constexpr auto operator<=>( + CacheKey, + CacheKey) = default; + friend inline constexpr bool operator==( + CacheKey, + CacheKey) = default; + }; + mutable base::flat_map _cachedBg; + mutable base::flat_map _cachedOutline; mutable std::unique_ptr _glare; Fn _repaint; @@ -134,12 +148,14 @@ KeyboardStyle::KeyboardStyle( , _repaint(std::move(repaint)) { } -void KeyboardStyle::startPaint( +void KeyboardStyle::paintButtonStart( QPainter &p, - const Ui::ChatStyle *st) const { + const Ui::ChatStyle *st, + HistoryMessageMarkupButton::Color color) const { + using Color = HistoryMessageMarkupButton::Color; Expects(st != nullptr); - p.setPen(st->msgServiceFg()); + p.setPen((color == Color::Normal) ? st->msgServiceFg() : st::white); } const style::TextStyle &KeyboardStyle::textStyle() const { @@ -175,12 +191,14 @@ void KeyboardStyle::paintButtonBg( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, Ui::BubbleRounding rounding, float64 howMuchOver) const { Expects(st != nullptr); using Corner = Ui::BubbleCornerRounding; - auto &cachedBg = _cachedBg[rounding.key()]; + const auto key = CacheKey{ rounding.key(), color }; + auto &cachedBg = _cachedBg[key]; const auto sti = &st->imageStyle(false); const auto ratio = style::DevicePixelRatio(); @@ -196,25 +214,50 @@ void KeyboardStyle::paintButtonBg( { auto painter = QPainter(&cachedBg.image); - const auto &small = sti->msgServiceBgCornersSmall; - const auto &large = sti->msgServiceBgCornersLarge; + using Color = HistoryMessageMarkupButton::Color; + const auto normal = (color == Color::Normal); + const auto colored = style::owned_color((color == Color::Primary) + ? QColor(0x37, 0x8e, 0xae) + : (color == Color::Danger) + ? QColor(0xc9, 0x54, 0x3e) + : QColor(0x48, 0x9d, 0x38)); + const auto smallColored = normal + ? Ui::CornersPixmaps() + : Ui::PrepareCornerPixmaps( + Ui::BubbleRadiusSmall(), + colored.color()); + const auto largeColored = normal + ? Ui::CornersPixmaps() + : Ui::PrepareCornerPixmaps( + Ui::BubbleRadiusLarge(), + colored.color()); + const auto small = normal + ? &sti->msgServiceBgCornersSmall + : &smallColored; + const auto large = normal + ? &sti->msgServiceBgCornersLarge + : &largeColored; auto corners = Ui::CornersPixmaps(); int radiuses[4]; for (auto i = 0; i != 4; ++i) { const auto isLarge = (rounding[i] == Corner::Large); - corners.p[i] = (isLarge ? large : small).p[i]; + corners.p[i] = (isLarge ? large : small)->p[i]; radiuses[i] = Ui::CachedCornerRadiusValue(isLarge ? Ui::CachedCornerRadius::BubbleLarge : Ui::CachedCornerRadius::BubbleSmall); } const auto r = Rect(rect.size()); - _cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath( + _cachedOutline[key] = Ui::ComplexRoundedRectPath( r - Margins(st::lineWidth), radiuses[0], radiuses[1], radiuses[2], radiuses[3]); - Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners); + Ui::FillRoundRect( + painter, + r, + normal ? sti->msgServiceBg : colored.color(), + corners); } } p.drawImage(rect.topLeft(), cachedBg.image); @@ -263,6 +306,7 @@ void KeyboardStyle::paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, + HistoryMessageMarkupButton::Color color, int outerWidth, Ui::BubbleRounding rounding) const { Expects(st != nullptr); @@ -277,8 +321,8 @@ void KeyboardStyle::paintButtonLoading( return; } - const auto cacheKey = rounding.key(); - auto &cachedBg = _cachedBg[cacheKey]; + const auto key = CacheKey{ rounding.key(), color }; + auto &cachedBg = _cachedBg[key]; if (!cachedBg.image.isNull()) { if (_glare && _glare->glare.birthTime) { const auto progress = _glare->progress(crl::now()); @@ -296,7 +340,7 @@ void KeyboardStyle::paintButtonLoading( auto path = QPainterPath(); path.addRect(Rect(rect.size())); - path -= _cachedOutline[cacheKey]; + path -= _cachedOutline[key]; constexpr auto kBgOutlineAlpha = 0.5; constexpr auto kFgOutlineAlpha = 0.8; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 8e15a6a1c7..8a976745f0 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_service_message.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_translate_tracker.h" +#include "history/view/history_view_top_peers_selector.h" #include "history/view/history_view_quick_action.h" #include "chat_helpers/message_field.h" #include "mainwindow.h" @@ -55,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" #include "boxes/delete_messages_box.h" @@ -2857,6 +2859,30 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : _overElement ? _overElement->data().get() : nullptr; + if (link + && link->property(kFastShareProperty).value() + && overItem) { + if (const auto view = viewForItem(overItem)) { + const auto rightSize = view->rightActionSize().value_or(QSize()); + const auto reactionsSkip = view->embedReactionsInBubble() + ? 0 + : view->reactionButtonParameters({}, {}).reactionsHeight; + const auto top = itemTop(view) + + view->height() + - reactionsSkip + - _visibleTop + - rightSize.height(); + const auto right = rect::right(view->innerGeometry()) + - st::historyFastShareLeft + - rightSize.width(); + ShowTopPeersSelector( + this, + controller()->uiShow(), + overItem->fullId(), + parentWidget()->mapToGlobal(QPoint(right, top))); + return; + } + } const auto clickedReaction = Reactions::ReactionIdOfLink(link); const auto linkPhoneNumber = link ? link->property(kPhoneNumberLinkProperty).toString() diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 0d6b2050bd..a13a846bea 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -45,7 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwidget.h" #include "main/main_session.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/text/text_options.h" #include "ui/painter.h" #include "window/themes/window_theme.h" // IsNightMode. @@ -629,7 +629,7 @@ QSize Message::performCountOptimalSize() { auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); maxWidth = textualWidth; - if (context() == Context::Replies && item->isDiscussionPost()) { + if (isCommentsRootView()) { maxWidth = std::max(maxWidth, st::msgMaxWidth); } minHeight = withVisibleText ? text().minHeight() : 0; @@ -1036,7 +1036,8 @@ void Message::draw(Painter &p, const PaintContext &context) const { context.st, messageRounding, g.width(), - context.clip.translated(-keyboardPosition)); + context.clip.translated(-keyboardPosition), + context.paused); p.translate(-keyboardPosition); } @@ -2369,7 +2370,7 @@ bool Message::hasFromPhoto() const { return true; } else if (item->isEmpty() || item->isFakeAboutView() - || (context() == Context::Replies && item->isDiscussionPost())) { + || isCommentsRootView()) { return false; } const auto mode = delegate()->elementChatMode(); @@ -3303,6 +3304,7 @@ Reactions::ButtonParameters Message::reactionButtonParameters( const auto reactionsHeight = (_reactions && !embedReactionsInBubble()) ? (st::mediaInBubbleSkip + _reactions->height()) : 0; + result.reactionsHeight = reactionsHeight; const auto innerHeight = geometry.height() - keyboardHeight - reactionsHeight; @@ -3778,7 +3780,7 @@ int Message::minWidthForMedia() const { bool Message::hasFastReply() const { if (context() == Context::Replies) { - if (data()->isDiscussionPost()) { + if (isCommentsRootView()) { return false; } } else if (context() != Context::History) { @@ -4078,7 +4080,17 @@ ClickHandlerPtr Message::prepareRightActionLink() const { } }; }; - return std::make_shared([=]( + + class FastShareClickHandler : public LambdaClickHandler { + public: + FastShareClickHandler(Fn handler) + : LambdaClickHandler(std::move(handler)) {} + QString tooltip() const override { + return tr::lng_fast_share_tooltip(tr::now); + } + }; + + const auto result = std::make_shared([=]( ClickContext context) { const auto controller = ExtractController(context); if (!controller || controller->session().uniqueId() != sessionId) { @@ -4100,6 +4112,8 @@ ClickHandlerPtr Message::prepareRightActionLink() const { } } }); + result->setProperty(kFastShareProperty, QVariant::fromValue(true)); + return result; } ClickHandlerPtr Message::fastReplyLink() const { @@ -4305,10 +4319,16 @@ QRect Message::innerGeometry() const { return result; } +bool Message::isCommentsRootView() const { + return context() == Context::Replies + && data()->isDiscussionPost() + && !data()->history()->isForum(); +} + QRect Message::countGeometry() const { const auto item = data(); const auto centeredView = item->isFakeAboutView() - || (context() == Context::Replies && item->isDiscussionPost()); + || isCommentsRootView(); const auto media = this->media(); const auto mediaWidth = (media && media->isDisplayed()) ? media->width() @@ -4374,7 +4394,7 @@ Ui::BubbleRounding Message::countMessageRounding() const { || (media && media->skipBubbleTail()) || (keyboard != nullptr) || item->isFakeAboutView() - || (context() == Context::Replies && item->isDiscussionPost()); + || isCommentsRootView(); const auto right = hasRightLayout(); using Corner = Ui::BubbleCornerRounding; return Ui::BubbleRounding{ @@ -4447,7 +4467,7 @@ int Message::resizeContentGetHeight(int newWidth) { // This code duplicates countGeometry() but also resizes media. const auto centeredView = item->isFakeAboutView() - || (context() == Context::Replies && item->isDiscussionPost()); + || isCommentsRootView(); const auto useMoreSpace = (delegate()->elementChatMode() == ElementChatMode::Narrow); const auto wideSkip = useMoreSpace diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 5d865850bc..07167e435b 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -291,6 +291,7 @@ private: [[nodiscard]] bool displayFastForward() const; [[nodiscard]] bool isPinnedContext() const; + [[nodiscard]] bool isCommentsRootView() const; [[nodiscard]] bool displayFastShare() const; [[nodiscard]] bool displayGoToOriginal() const; diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp index adda745bd7..0d1abb053e 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp @@ -106,7 +106,6 @@ constexpr auto kPremiumToastDuration = 5 * crl::time(1000); (result->height() - st::toastUndoDiameter) / 2, st::toastUndoDiameter, st::toastUndoDiameter); - p.setFont(st::toastUndoFont); state->countdown.paint( p, inner.x() + (inner.width() - state->countdown.countWidth()) / 2, diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 2ac4e7c5d3..0954693897 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -827,7 +827,7 @@ void Reply::paint( } if (_ripple.animation) { - _ripple.lastPaintedPoint = { x, y }; + _ripple.lastPaintedPoint = inBubble ? QPoint(x, y) : QPoint(); _ripple.animation->paint(p, x, y, w, &cache->bg2); if (_ripple.animation->empty()) { _ripple.animation.reset(); @@ -993,7 +993,11 @@ void Reply::createRippleAnimation( Ui::RippleAnimation::RoundRectMask( size, st::messageQuoteStyle.radius), - [=] { view->repaint(QRect(_ripple.lastPaintedPoint, size)); }); + [=] { + view->repaint(_ripple.lastPaintedPoint.isNull() + ? QRect() + : QRect(_ripple.lastPaintedPoint, size)); + }); } void Reply::saveRipplePoint(QPoint point) const { diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp index e540660cb8..592f548c19 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp @@ -24,7 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/padding_wrap.h" #include "main/main_session.h" #include "menu/menu_send.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_chat.h" diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 677bae1c05..b2af07bd0c 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -656,7 +656,8 @@ void Service::draw(Painter &p, const PaintContext &context) const { context.st, KeyboardRounding(), keyboardWidth, - context.clip.translated(-keyboardPosition)); + context.clip.translated(-keyboardPosition), + context.paused); p.translate(-keyboardPosition); } @@ -969,7 +970,8 @@ void EmptyPainter::paint( if (_icon) { _icon->paintInRect( p, - QRect(bubbleLeft, top, bubbleWidth, iconHeight)); + QRect(bubbleLeft, top, bubbleWidth, iconHeight), + st->msgServiceFg()->c); top += iconHeight + st::historyGroupAboutHeaderSkip; } diff --git a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp index 92ac7fefaa..db494e5d02 100644 --- a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp @@ -21,7 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "lottie/lottie_single_player.h" #include "window/window_session_controller.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "apiwrap.h" #include "styles/style_chat.h" diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index f625531583..195c7aa205 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -453,15 +453,15 @@ void SubsectionTabs::startFillingSlider( ).append(' ').append(peer->shortName()), }); } - } else if (item.thread->peer()->isBot()) { - sections.push_back({ - .text = { tr::lng_bot_new_chat(tr::now) }, - }); - if (vertical) { - auto &last = sections.back(); - last.userpic = Ui::MakeNewChatSubsectionsThumbnail( - textFg); - } + // } else if (Data::IsBotCanManageTopics(item.thread->peer())) { + // sections.push_back({ + // .text = { tr::lng_bot_new_chat(tr::now) }, + // }); + // if (vertical) { + // auto &last = sections.back(); + // last.userpic = Ui::MakeNewChatSubsectionsThumbnail( + // textFg); + // } } else { sections.push_back({ .text = { tr::lng_filters_all_short(tr::now) }, diff --git a/Telegram/SourceFiles/history/view/history_view_text_helper.cpp b/Telegram/SourceFiles/history/view/history_view_text_helper.cpp index fbe97ed567..4fe5af1c39 100644 --- a/Telegram/SourceFiles/history/view/history_view_text_helper.cpp +++ b/Telegram/SourceFiles/history/view/history_view_text_helper.cpp @@ -7,10 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_text_helper.h" +#include "boxes/sticker_set_box.h" +#include "core/click_handler_types.h" +#include "data/data_document.h" #include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "main/main_session.h" +#include "window/window_session_controller.h" #include "base/weak_ptr.h" namespace HistoryView { @@ -36,6 +41,28 @@ void InitElementTextPart(not_null view, Ui::Text::String &text) { } }); } + if (text.hasCustomEmoji()) { + text.setCustomEmojiClickHandler( + [](QStringView entityData) { + return Data::ParseCustomEmojiData(entityData) != 0; + }, + [weak = base::make_weak(view)]( + QStringView entityData, + ClickContext context) { + const auto view = weak.get(); + if (!view) { + return; + } + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + const auto documentId = Data::ParseCustomEmojiData(entityData); + if (documentId) { + const auto document = controller->session().data().document(documentId); + StickerSetBox::Show(controller->uiShow(), document, documentId); + } + } + }); + } } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 6d33e9479f..62dbd64558 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_send_action.h" #include "dialogs/dialogs_main_list.h" #include "chat_helpers/emoji_interactions.h" +#include "base/call_delayed.h" #include "base/unixtime.h" #include "support/support_helper.h" #include "apiwrap.h" @@ -84,16 +85,6 @@ namespace { constexpr auto kEmojiInteractionSeenDuration = 3 * crl::time(1000); -class MenuToggleButton final : public Ui::IconButton { -public: - using IconButton::IconButton; - -protected: - void contextMenuEvent(QContextMenuEvent *e) override { - Ui::AbstractButton::clicked(Qt::KeyboardModifiers(), Qt::LeftButton); - } -}; - [[nodiscard]] inline bool HasGroupCallMenu(not_null peer) { return !peer->isUser() && !peer->groupCall() @@ -145,9 +136,7 @@ TopBarWidget::TopBarWidget( , _groupCall(this, st::topBarGroupCall) , _search(this, st::topBarSearch) , _infoToggle(this, st::topBarInfo) -, _menuToggle( - object_ptr::fromRaw( - Ui::CreateChild(this, st::topBarMenuToggle))) +, _menuToggle(this, st::topBarMenuToggle) , _recentActions(this, st::topBarRecentActions) , _admins(this, st::topBarAdmins) , _titlePeerText(st::windowMinWidth / 3) @@ -168,9 +157,16 @@ TopBarWidget::TopBarWidget( _messageShot->setClickedCallback([=] { _messageShotSelection.fire({}); }); _messageShot->setWidthChangedCallback([=] { updateControlsGeometry(); }); _clear->setClickedCallback([=] { _clearSelection.fire({}); }); - _call->setClickedCallback([=] { call(); }); + _call->setClickedCallback([=] { call({}); }); + _call->setAcceptBoth(true, true); + _call->addClickHandler([=](Qt::MouseButton button) { + if (button == Qt::RightButton) { + showCallMenu(); + } + }); _groupCall->setClickedCallback([=] { groupCall(); }); - _menuToggle->setClickedCallback([=] { showPeerMenu(); }); + _menuToggle->addClickHandler([=](auto) { showPeerMenu(); }); + _menuToggle->setAcceptBoth(true, true); _infoToggle->setClickedCallback([=] { toggleInfoSection(); }); _recentActions->setClickedCallback([=] @@ -280,6 +276,13 @@ TopBarWidget::TopBarWidget( }, lifetime()); setCursor(style::cur_pointer); + _call->setAccessibleName(tr::lng_profile_action_short_call(tr::now)); + _groupCall->setAccessibleName(tr::lng_group_call_title(tr::now)); + _search->setAccessibleName(tr::lng_shortcuts_search(tr::now)); + _infoToggle->setAccessibleName(tr::lng_settings_section_info(tr::now)); + _menuToggle->setAccessibleName(tr::lng_chat_menu(tr::now)); + _back->setAccessibleName(tr::lng_go_back(tr::now)); + _cancelChoose->setAccessibleName(tr::lng_cancel(tr::now)); } TopBarWidget::~TopBarWidget() = default; @@ -317,12 +320,12 @@ void TopBarWidget::refreshLang() { InvokeQueued(this, [this] { updateControlsGeometry(); }); } -void TopBarWidget::call() { +void TopBarWidget::call(Calls::StartOutgoingCallArgs args) { if (_controller->showFrozenError()) { return; } else if (const auto peer = _activeChat.key.peer()) { if (const auto user = peer->asUser()) { - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, std::move(args)); } } } @@ -371,13 +374,17 @@ void TopBarWidget::setChooseForReportReason( : style::cur_default); } -bool TopBarWidget::createMenu(not_null button) { +bool TopBarWidget::createMenu( + not_null button, + bool withIcons) { if (!_activeChat.key || _menu) { return false; } _menu = base::make_unique_q( this, - st::popupMenuExpandedSeparator); + withIcons + ? st::popupMenuExpandedSeparator + : st::defaultPopupMenu); _menu->setDestroyedCallback([ weak = base::make_weak(this), weakButton = base::make_weak(button), @@ -403,9 +410,14 @@ void TopBarWidget::showPeerMenu() { _menu = nullptr; } else { _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); - _menu->popup(mapToGlobal(QPoint( - width() + st::topBarMenuPosition.x(), - st::topBarMenuPosition.y()))); + _menu->popup(Ui::PopupMenu::ConstrainToParentScreen( + _menu, + mapToGlobal( + QPoint( + width() + + st::topBarMenuPosition.x() + + _menu->st().shadow.extend.right(), + st::topBarMenuPosition.y())))); } } @@ -422,6 +434,30 @@ void TopBarWidget::showGroupCallMenu(not_null peer) { st::topBarMenuPosition.y()))); } +void TopBarWidget::showCallMenu() { + const auto created = createMenu(_call, false); + if (!created) { + return; + } + const auto perform = [&](bool video) { + return [=] { + base::call_delayed(st::defaultPopupMenu.showDuration, this, [=] { + call({ .video = video, .isConfirmed = true }); + }); + }; + }; + _menu->addAction( + tr::lng_profile_action_short_call(tr::now), + perform(false)); + _menu->addAction( + tr::lng_call_start_video(tr::now), + perform(true)); + _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + _menu->popup(mapToGlobal(QPoint( + _call->x() + _call->width() + st::topBarMenuGroupCallSkip, + st::topBarMenuPosition.y()))); +} + void TopBarWidget::toggleInfoSection() { const auto isThreeColumn = _controller->adaptive().isThreeColumn(); if (isThreeColumn @@ -1003,6 +1039,13 @@ void TopBarWidget::refreshInfoButton() { } if (_info) { _info->setAttribute(Qt::WA_TransparentForMouseEvents); + _info->setAccessibleName(tr::lng_settings_section_info(tr::now)); + if (_back && _info) { + QWidget::setTabOrder(_back.data(), _info.data()); + } + if (_info && _search) { + QWidget::setTabOrder(_info.data(), _search.data()); + } } } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index 065cf1601f..4072feb3f1 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -36,6 +36,10 @@ namespace Window { class SessionController; } // namespace Window +namespace Calls { +struct StartOutgoingCallArgs; +} // namespace Calls + namespace HistoryView { class SendActionPainter; @@ -144,12 +148,15 @@ private: void updateInfoToggleActive(); void setupDragOnBackButton(); - void call(); + void call(Calls::StartOutgoingCallArgs); void groupCall(); void showGroupCallMenu(not_null peer); + void showCallMenu(); void toggleInfoSection(); - [[nodiscard]] bool createMenu(not_null button); + [[nodiscard]] bool createMenu( + not_null button, + bool withIcons = true); void handleEmojiInteractionSeen(const QString &emoticon); bool paintSendAction( diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp new file mode 100644 index 0000000000..50644eea78 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -0,0 +1,217 @@ +// 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 "history/view/history_view_top_peers_selector.h" + +#include "apiwrap.h" +#include "base/unique_qptr.h" +#include "chat_helpers/share_message_phrase_factory.h" +#include "data/components/top_peers.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "main/session/session_show.h" +#include "ui/controls/dynamic_images_strip.h" +#include "ui/controls/popup_selector.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/effects/animations.h" +#include "ui/rect.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/wrap/padding_wrap.h" +#include "styles/style_chat_helpers.h" + +namespace HistoryView { +namespace { + +constexpr auto kMaxPeers = 5; + +[[nodiscard]] std::vector> CollectPeers( + not_null session) { + const auto user = session->user(); + auto topPeers = session->topPeers().list(); + const auto it = ranges::find(topPeers, user); + if (it != topPeers.end()) { + topPeers.erase(it); + } + auto result = std::vector>(); + result.push_back(user); + for (const auto &peer : topPeers | ranges::views::take(kMaxPeers - 1)) { + result.push_back(peer); + } + return result; +} + +} // namespace + +void ShowTopPeersSelector( + not_null parent, + std::shared_ptr show, + FullMsgId fullId, + QPoint globalPos) { + const auto session = &show->session(); + const auto peers = CollectPeers(session); + auto thumbnails = std::vector>(); + thumbnails.reserve(peers.size()); + for (const auto &peer : peers) { + thumbnails.push_back(peer->isSelf() + ? Ui::MakeSavedMessagesThumbnail() + : Ui::MakeUserpicThumbnail(peer)); + } + + const auto send = [=](not_null peer) { + if (const auto item = session->data().message(fullId)) { + session->api().forwardMessages( + Data::ResolvedForwardDraft{ .items = { item } }, + Api::SendAction(session->data().history(peer)), + [=] { + using namespace ChatHelpers; + auto text = rpl::variable( + ForwardedMessagePhrase({ + .toCount = 1, + .singleMessage = 1, + .to1 = peer, + })).current(); + show->showToast(std::move(text)); + }); + } + }; + + const auto contentWidth = peers.size() * st::topPeersSelectorUserpicSize + + (peers.size() - 1) * st::topPeersSelectorUserpicGap; + const auto contentHeight = int( + st::topPeersSelectorUserpicSize + * (1. + st::topPeersSelectorUserpicExpand)); + const auto selectorWidth = contentWidth + + 2 * st::topPeersSelectorPadding; + const auto selectorHeight = contentHeight + + 2 * st::topPeersSelectorPadding; + + struct State { + base::unique_qptr selector; + base::unique_qptr tooltip; + Ui::Animations::Simple animation; + bool finishing = false; + }; + const auto state = std::make_shared(); + + state->selector = base::make_unique_q( + parent, + QSize(selectorWidth, selectorHeight)); + const auto selector = state->selector.get(); + selector->setHideFinishedCallback([=, state = std::weak_ptr(state)] { + if (const auto s = state.lock()) { + s->selector = nullptr; + s->tooltip = nullptr; + } + }); + const auto userpicsWidget = Ui::CreateChild( + selector, + std::move(thumbnails), + st::topPeersSelectorUserpicSize, + st::topPeersSelectorUserpicGap); + const auto margins = selector->marginsForShadow(); + const auto x = (selectorWidth - contentWidth) / 2 + margins.left(); + const auto y = (selectorHeight - contentHeight) / 2 + margins.top(); + userpicsWidget->setGeometry( + QRect(x, y, contentWidth, contentHeight) + + Margins(int(st::topPeersSelectorUserpicSize + * st::topPeersSelectorUserpicExpand))); + userpicsWidget->setCursor(style::cur_pointer); + + const auto hideAll = [=] { + state->finishing = true; + if (state->tooltip) { + state->tooltip->toggleAnimated(false); + } + selector->setAttribute(Qt::WA_TransparentForMouseEvents, true); + selector->hideAnimated(); + }; + + userpicsWidget->setClickCallback([=](int index) { + if (state->finishing) { + return; + } + send(peers[index]); + hideAll(); + }); + userpicsWidget->hoveredItemValue( + ) | rpl::on_next([=](Ui::HoveredItemInfo info) { + if (info.index < 0) { + state->tooltip = nullptr; + return; + } + using namespace Info::Profile; + state->tooltip = base::make_unique_q( + parent, + object_ptr>( + selector, + Ui::MakeNiceTooltipLabel( + parent, + peers[info.index]->isSelf() + ? tr::lng_saved_messages(tr::rich) + : NameValue(peers[info.index]) | rpl::map(tr::rich), + userpicsWidget->width(), + st::topPeersSelectorImportantTooltipLabel), + st::topPeersSelectorImportantTooltip.padding), + st::topPeersSelectorImportantTooltip); + state->tooltip->setWindowFlags(Qt::WindowFlags(Qt::ToolTip) + | Qt::BypassWindowManagerHint + | Qt::NoDropShadowWindowHint + | Qt::FramelessWindowHint); + state->tooltip->setAttribute(Qt::WA_NoSystemBackground, true); + state->tooltip->setAttribute(Qt::WA_TranslucentBackground, true); + state->tooltip->setAttribute(Qt::WA_TransparentForMouseEvents, true); + const auto step = st::topPeersSelectorUserpicSize + + st::topPeersSelectorUserpicGap; + const auto shift = (userpicsWidget->height() + - st::topPeersSelectorUserpicSize) / 2; + const auto localX = info.index * step + shift; + const auto avatarRect = QRect( + localX, + -shift, + st::topPeersSelectorUserpicSize, + st::topPeersSelectorUserpicSize); + const auto globalRect = QRect( + userpicsWidget->mapToGlobal(avatarRect.topLeft()), + avatarRect.size()); + state->tooltip->pointAt(globalRect, RectPart::Top); + state->tooltip->toggleAnimated(true); + }, selector->lifetime()); + selector->updateShowState(0, 0, true); + selector->popup((!globalPos.isNull() ? globalPos : QCursor::pos()) + - QPoint(selector->width() / 2, selector->height()) + + st::topPeersSelectorSkip); + selector->events( + ) | rpl::on_next([=](not_null e) { + if (e->type() == QEvent::KeyPress) { + const auto key = static_cast(e.get()); + if (key->key() == Qt::Key_Escape) { + hideAll(); + } else { + userpicsWidget->handleKeyPressEvent(key); + } + } + }, selector->lifetime()); + crl::on_main(selector, [=] { + selector->setFocus(); + }); + + constexpr auto kShift = 0.15; + state->animation.start([=](float64 value) { + const auto userpicsProgress = std::clamp((value - kShift), 0., 1.); + userpicsWidget->setProgress(anim::easeInQuint(1, userpicsProgress)); + value = std::clamp(value, 0., 1.); + selector->updateShowState(value, value, true); + }, 0., 1. + kShift, st::fadeWrapDuration * 3, anim::easeOutQuint); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h new file mode 100644 index 0000000000..6de3b03f8e --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h @@ -0,0 +1,25 @@ +// 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 RpWidget; +} // namespace Ui + +namespace Main { +class SessionShow; +} // namespace Main + +namespace HistoryView { + +void ShowTopPeersSelector( + not_null parent, + std::shared_ptr show, + FullMsgId fullId, + QPoint globalPos); + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp index 8a3a5606fa..da82a5f728 100644 --- a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp @@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "main/main_session.h" #include "lang/lang_keys.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" diff --git a/Telegram/SourceFiles/history/view/media/history_view_call.cpp b/Telegram/SourceFiles/history/view/media/history_view_call.cpp index 6cfd8c938b..0e58229f31 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_call.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_call.cpp @@ -76,7 +76,7 @@ QSize Call::countOptimalSize() { strong->resolveConferenceCall(id, contextId); } } else if (user) { - Core::App().calls().startOutgoingCall(user, video); + Core::App().calls().startOutgoingCall(user, { video }); } }); auto maxWidth = st::historyCallWidth; diff --git a/Telegram/SourceFiles/history/view/media/history_view_dice.cpp b/Telegram/SourceFiles/history/view/media/history_view_dice.cpp index 93eed91e1f..17120a6fe7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_dice.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_dice.cpp @@ -63,8 +63,18 @@ void Dice::updateOutcomeMessage() { return; } const auto item = _parent->data(); - const auto from = item->from(); - const auto out = item->out() || from->isSelf(); + const auto forwarded = item->Get(); + const auto originalSender = forwarded + ? forwarded->originalSender + : nullptr; + const auto from = originalSender ? originalSender : item->from().get(); + const auto originalPostAuthor = item->originalPostAuthor(); + const auto fromName = !originalPostAuthor.isEmpty() + ? originalPostAuthor + : forwarded && forwarded->originalHiddenSenderInfo + ? forwarded->originalHiddenSenderInfo->name + : from->name(); + const auto out = (item->out() || from->isSelf()) && !forwarded; const auto won = (_outcomeNanoTon - _outcomeStakeNanoTon); const auto amount = tr::marked(QString::fromUtf8("\xf0\x9f\x92\x8e") + " " @@ -82,13 +92,15 @@ void Dice::updateOutcomeMessage() { : tr::lng_action_stake_game_lost)( tr::now, lt_from, - tr::link(st::wrap_rtl(from->name()), 1), + tr::link(st::wrap_rtl(fromName), 1), lt_amount, amount, tr::marked); auto prepared = PreparedServiceText{ text }; if (!out) { - prepared.links.push_back(from->createOpenLink()); + if (const auto link = forwarded ? originalSender : from) { + prepared.links.push_back(link->createOpenLink()); + } } _parent->setServicePostMessage(prepared, _link); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index 8a504d6654..984651b68f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -391,6 +391,35 @@ QSize TextDelimeterPart::countCurrentSize(int newWidth) { return { newWidth, minHeight() }; } +LambdaGenericPart::LambdaGenericPart( + QSize size, + Fn owner, + const PaintContext &context, + int outerWidth)> draw) +: _size(size) +, _draw(std::move(draw)) { +} + +void LambdaGenericPart::draw( + Painter &p, + not_null owner, + const PaintContext &context, + int outerWidth) const { + if (_draw) { + _draw(p, owner, context, outerWidth); + } +} + +QSize LambdaGenericPart::countOptimalSize() { + return _size; +} + +QSize LambdaGenericPart::countCurrentSize(int newWidth) { + return { newWidth, _size.height() }; +} + StickerInBubblePart::StickerInBubblePart( not_null parent, Element *replacing, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index 68ee1886e4..2251ba8840 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -194,6 +194,35 @@ private: }; +class LambdaGenericPart final : public MediaGenericPart { +public: + LambdaGenericPart( + QSize size, + Fn owner, + const PaintContext &context, + int outerWidth)> draw); + + void draw( + Painter &p, + not_null owner, + const PaintContext &context, + int outerWidth) const override; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + +private: + QSize _size; + Fn owner, + const PaintContext &context, + int outerWidth)> _draw; + +}; + class StickerInBubblePart final : public MediaGenericPart { public: struct Data { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index 3e40020ebe..43db062173 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -584,7 +584,10 @@ QPoint UnwrappedMedia::resolveCustomInfoRightBottom() const { const auto fullRight = calculateFullRight(inner); const auto skipx = st::msgDateImgPadding.x(); const auto skipy = st::msgDateImgPadding.y(); - return QPoint(fullRight - skipx, fullBottom - skipy); + const auto infoWidth = _parent->infoWidth() + + st::msgDateImgPadding.x() * 2 + + st::msgReplyPadding.left(); + return QPoint(fullRight - skipx - infoWidth, fullBottom - skipy); } std::unique_ptr UnwrappedMedia::stickerTakePlayer( diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 6b10d54127..63c98b5aac 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -26,9 +26,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "settings/settings_credits.h" // Settings::CreditsId +#include "settings/sections/settings_credits.h" // Settings::CreditsId #include "settings/settings_credits_graphics.h" // GiftedCreditsBox -#include "settings/settings_premium.h" // Settings::ShowGiftPremium +#include "settings/sections/settings_premium.h" // Settings::ShowGiftPremium #include "ui/chat/chat_style.h" #include "ui/controls/ton_common.h" // kNanosInOne #include "ui/layers/generic_box.h" @@ -361,11 +361,15 @@ void PremiumGift::draw( QImage PremiumGift::cornerTag(const PaintContext &context) { auto badge = Info::PeerGifts::GiftBadge(); if (_data.unique) { + const auto burned = _data.unique->burned; + const auto burnedBg = Info::PeerGifts::BurnedBadgeBg(); badge = { - .text = tr::lng_gift_collectible_tag(tr::now), - .bg1 = _data.unique->backdrop.edgeColor, - .bg2 = _data.unique->backdrop.patternColor, - .fg = QColor(255, 255, 255), + .text = (burned + ? tr::lng_gift_burned_tag(tr::now) + : tr::lng_gift_collectible_tag(tr::now)), + .bg1 = (burned ? burnedBg : _data.unique->backdrop.edgeColor), + .bg2 = (burned ? burnedBg : _data.unique->backdrop.patternColor), + .fg = (burned ? st::white->c : _data.unique->backdrop.textColor), }; } else if (const auto count = _data.limitedCount) { badge = { @@ -528,6 +532,9 @@ ClickHandlerPtr OpenStarGiftLink(not_null item) { const auto controller = weak.get(); if (!controller) { return; + } else if (data.unique && data.unique->burned) { + controller->showToast(tr::lng_gift_burned_message(tr::now)); + return; } const auto quick = [=](not_null window) { Settings::ShowStarGiftViewBox(window, data, itemId); diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp index 70bb4dc44f..b15caf1e13 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp @@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_memento.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/ripple_animation.h" diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp index 7356e7b034..534a74d588 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp @@ -35,7 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "apiwrap.h" #include "api/api_peer_photo.h" -#include "settings/settings_information.h" // UpdatePhotoLocally +#include "settings/sections/settings_information.h" // UpdatePhotoLocally #include "styles/style_chat.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp index 038a7a5214..eb39ee065d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -361,7 +361,9 @@ auto GenerateUniqueGiftMedia( const auto peer = parent->history()->peer; pushText( tr::bold(peer->isSelf() - ? tr::lng_action_gift_self_subtitle(tr::now) + ? (gift->crafted + ? tr::lng_action_gift_crafted_subtitle(tr::now) + : tr::lng_action_gift_self_subtitle(tr::now)) : peer->isServiceUser() ? tr::lng_gift_link_label_gift(tr::now) : (outgoing @@ -493,11 +495,16 @@ auto UniqueGiftBg( ? QMargins() : st::chatUniqueGiftBadgePadding; p.setClipRect(inner.marginsAdded(padding)); + + const auto burned = gift->burned; + const auto burnedBg = Info::PeerGifts::BurnedBadgeBg(); auto badge = Info::PeerGifts::GiftBadge{ - .text = tr::lng_gift_collectible_tag(tr::now), - .bg1 = gift->backdrop.edgeColor, - .bg2 = gift->backdrop.patternColor, - .fg = gift->backdrop.textColor, + .text = (burned + ? tr::lng_gift_burned_tag(tr::now) + : tr::lng_gift_collectible_tag(tr::now)), + .bg1 = (burned ? burnedBg : gift->backdrop.edgeColor), + .bg2 = (burned ? burnedBg : gift->backdrop.patternColor), + .fg = (burned ? st::white->c : gift->backdrop.textColor), }; if (state->badgeCache.isNull() || state->badgeKey != badge) { state->badgeKey = badge; diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp index 15a7558e80..7eb6dd055c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp @@ -30,7 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "apiwrap.h" #include "api/api_peer_photo.h" -#include "settings/settings_information.h" // UpdatePhotoLocally +#include "settings/sections/settings_information.h" // UpdatePhotoLocally #include "styles/style_chat.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h index 4a1842d995..75e6d45384 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h @@ -55,6 +55,7 @@ struct ButtonParameters { QPoint center; QPoint pointer; QPoint globalPointer; + int reactionsHeight = 0; int reactionsCount = 1; int visibleTop = 0; int visibleBottom = 0; diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp index a187928026..ca66330a8b 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp @@ -26,7 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_checkout_process.h" // Payments::CheckoutProcess #include "payments/payments_form.h" // Payments::InvoicePremiumGiftCode #include "settings/settings_common.h" -#include "settings/settings_premium.h" // Settings::ShowPremium +#include "settings/sections/settings_premium.h" // Settings::ShowPremium #include "ui/boxes/choose_date_time.h" #include "ui/boxes/confirm_box.h" #include "ui/effects/credits_graphics.h" diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index 7e2141199d..86207a02e2 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -230,6 +230,7 @@ darkGiftLink: icon {{ "menu/copy", groupCallMembersFg }}; darkGiftShare: icon {{ "menu/share", groupCallMembersFg }}; darkGiftTheme: icon {{ "menu/colors", groupCallMembersFg }}; darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }}; +darkGiftCraft: icon {{ "menu/craft_start-24x24", groupCallMembersFg }}; darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }}; darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }}; darkGiftNftResell: icon {{ "menu/tag_sell", groupCallMembersFg }}; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 18b068f50e..3b6d2482c8 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -169,6 +169,7 @@ infoTopBarMenu: IconButton(infoTopBarBack) { iconPosition: point(18px, -1px); rippleAreaPosition: point(1px, 6px); } +infoTopBarMenuActive: icon {{ "title_menu_dots", windowActiveTextFg, point(0px, 0px) }}; infoTopBarCall: IconButton(infoTopBarMenu) { width: 42px; icon: icon {{ "top_bar_call", boxTitleCloseFg }}; @@ -289,6 +290,12 @@ infoLayerTopBarQr: IconButton(infoLayerTopBarClose) { iconOver: icon {{ "menu/qr_code", boxTitleCloseFgOver }}; iconPosition: point(8px, -1px); } +infoLayerTopBarSearch: IconButton(infoLayerTopBarClose) { + width: 48px; + icon: icon {{ "top_bar_search", boxTitleCloseFg }}; + iconOver: icon {{ "top_bar_search", boxTitleCloseFgOver }}; + iconPosition: point(7px, -1px); +} infoLayerTopBarEdit: IconButton(infoLayerTopBarClose) { width: 44px; height: infoLayerTopBarHeight - 2px; @@ -595,13 +602,14 @@ infoRatingDeductedBadge: RoundButton(customEmojiTextBadge) { infoProfileInaccessibleUserpic: icon {{ "info/inaccessible_userpic", historyPeerUserpicFg }}; +infoVerifiedCheck: icon {{ "profile_verified_check", profileVerifiedCheckFg }}; infoVerifiedCheckPosition: point(4px, 0px); infoVerifiedStar: icon {{ "profile_verified_star", profileVerifiedCheckBg }}; infoPremiumStar: icon {{ "profile_premium", profileVerifiedCheckBg }}; infoPeerBadge: InfoPeerBadge { verified: infoVerifiedStar; - verifiedCheck: icon {{ "profile_verified_check", profileVerifiedCheckFg }}; + verifiedCheck: infoVerifiedCheck; premium: infoPremiumStar; premiumFg: profileVerifiedCheckBg; position: infoVerifiedCheckPosition; @@ -637,6 +645,7 @@ infoProfileLabeledButtonQr: IconButton(defaultIconButton) { ripple: defaultRippleAnimation; } infoProfileLabeledButtonQrRightSkip: 0px; +infoProfileLabeledButtonQrInset: 5px; infoIconInformation: icon {{ "info/info_information", infoIconFg }}; infoIconAddMember: icon {{ "info/info_add_member", infoIconFg }}; @@ -667,6 +676,7 @@ infoIconReport: icon {{ "info/info_report", attentionButtonFg }}; infoIconLeave: icon {{ "info/info_leave", infoIconFg }}; infoIconBlock: icon {{ "info/info_block", attentionButtonFg }}; infoIconMembers: icon {{ "info/info_members", infoIconFg }}; +infoIconPrivacyPolicy: icon {{ "menu/2sv_off", infoIconFg, point(4px, 4px) }}; infoInformationIconPosition: point(25px, 12px); infoNotificationsIconPosition: point(20px, 5px); infoSharedMediaButtonIconPosition: point(20px, 3px); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 1e7cb8ee42..8cbdcb37a0 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -16,17 +16,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_memento.h" #include "info/info_top_bar.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" -#include "settings/settings_chat.h" -#include "settings/settings_information.h" -#include "settings/settings_main.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_chat.h" +#include "settings/sections/settings_information.h" +#include "settings/sections/settings_main.h" +#include "settings/sections/settings_premium.h" +#include "settings/settings_search.h" #include "ui/effects/ripple_animation.h" // MaskByDrawer. #include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/menu/menu_item_base.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" #include "ui/widgets/popup_menu.h" -#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/wrap/fade_wrap.h" #include "ui/search_field_controller.h" #include "ui/ui_utility.h" @@ -171,15 +173,30 @@ WrapWidget::WrapWidget( } void WrapWidget::setupShortcuts() { + const auto isSettings = [=] { + return _controller->section().type() == Section::Type::Settings; + }; + const auto isSearchSettings = [=] { + return isSettings() + && (_controller->section().settingsType() + == ::Settings::Search::Id()); + }; + Shortcuts::Requests( ) | rpl::filter([=] { - return requireTopBarSearch() - && (Core::App().activeWindow() - == &_controller->parentController()->window()); + return (Core::App().activeWindow() + == &_controller->parentController()->window()) + && (requireTopBarSearch() || isSettings()); }) | rpl::on_next([=](not_null request) { using Command = Shortcuts::Command; request->check(Command::Search) && request->handle([=] { - _topBar->showSearch(); + if (requireTopBarSearch()) { + _topBar->showSearch(); + } else if (isSearchSettings()) { + _content->setInnerFocus(); + } else if (isSettings()) { + _controller->showSettings(::Settings::Search::Id()); + } return true; }); }, lifetime()); @@ -419,8 +436,16 @@ void WrapWidget::setupTopBarMenuToggle() { addProfileCallsButton(); } else if (section.type() == Section::Type::Settings) { addTopBarMenuButton(); - if (section.settingsType() == ::Settings::Information::Id() - || section.settingsType() == ::Settings::Main::Id()) { + if (section.settingsType() == ::Settings::MainId()) { + const auto &st = (wrap() == Wrap::Layer) + ? st::infoLayerTopBarSearch + : st::infoTopBarSearch; + const auto button = _topBar->addButton( + base::make_unique_q(_topBar, st)); + button->addClickHandler([=] { + _controller->showSettings(::Settings::Search::Id()); + }); + } else if (section.settingsType() == ::Settings::InformationId()) { const auto controller = _controller->parentController(); const auto self = controller->session().user(); if (!self->username().isEmpty()) { @@ -482,6 +507,7 @@ void WrapWidget::setupTopBarMenuToggle() { }); } } + setupShortcuts(); } else if (key.storiesPeer() && key.storiesPeer()->isSelf() && key.storiesAlbumId() != Stories::ArchiveId()) { @@ -491,7 +517,7 @@ void WrapWidget::setupTopBarMenuToggle() { const auto button = _topBar->addButton( base::make_unique_q(_topBar, st)); button->addClickHandler([=] { - _controller->showSettings(::Settings::Information::Id()); + _controller->showSettings(::Settings::InformationId()); }); } else if (section.type() == Section::Type::Downloads) { auto &manager = Core::App().downloadManager(); @@ -606,7 +632,7 @@ void WrapWidget::addProfileCallsButton() { ? st::infoLayerTopBarCall : st::infoTopBarCall)) )->addClickHandler([=] { - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, {}); }); }, _topBar->lifetime()); @@ -856,6 +882,36 @@ void WrapWidget::showFinishedHook() { _bottomShadow->toggle(_bottomShadow->toggled(), anim::type::instant); _topBarSurrogate.destroy(); _content->showFinished(); + + if (_topBarMenuToggle + && _controller->section().type() == Section::Type::Settings) { + const auto controller = _controller->parentController(); + const auto settingsType = _controller->section().settingsType(); + const auto highlightId = [&]() -> QString { + if (settingsType == ::Settings::MainId()) { + return u"settings/log-out"_q; + } else if (settingsType == ::Settings::ChatId()) { + return u"chat/themes-create"_q; + } + return QString(); + }(); + if (!highlightId.isEmpty() + && controller->takeHighlightControlId(highlightId)) { + showTopBarMenu(false); + if (_topBarMenu) { + const auto menu = _topBarMenu->menu(); + for (const auto action : menu->actions()) { + const auto controlId = "highlight-control-id"; + if (action->property(controlId).toString() == highlightId) { + if (const auto item = menu->itemForAction(action)) { + ::Settings::HighlightWidget(item); + } + break; + } + } + } + } + } } bool WrapWidget::showInternal( diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index cd0ea6fc25..0302091930 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -47,6 +47,7 @@ namespace Info::PeerGifts { namespace { constexpr auto kGiftsPerRow = 3; +constexpr auto kCraftUnavailableOpacity = 0.5; [[nodiscard]] bool AllowedToSend( const GiftTypeStars &gift, @@ -199,7 +200,9 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { const auto unique = v::is(descriptor) ? v::get(descriptor).info.unique.get() : nullptr; - const auto resalePrice = unique ? unique->starsForResale : 0; + const auto resalePrice = (unique && _mode != Mode::CraftPreview) + ? unique->starsForResale + : 0; if (_descriptor == descriptor && _resalePrice == resalePrice) { return; } @@ -263,12 +266,12 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { const auto soldOut = data.info.limitedCount && !data.userpic && !data.info.limitedLeft; - _userpic = (!data.userpic || _mode == GiftButtonMode::Selection) + _userpic = (!data.userpic || (_mode == Mode::Selection)) ? nullptr : data.from ? Ui::MakeUserpicThumbnail(data.from) : Ui::MakeHiddenAuthorThumbnail(); - if (small() && !resale) { + if ((small() && !resale) || (_mode == Mode::Craft)) { _price = {}; _stars.reset(); return; @@ -335,7 +338,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { _uniquePatternEmoji = nullptr; _uniquePatternCache.clear(); - if (small() && !resale) { + if (_price.isEmpty()) { _button = QRect(); return; } @@ -403,18 +406,18 @@ void GiftButton::setDocument(not_null document) { ChatHelpers::LottiePlayerFromDocument( media.get(), ChatHelpers::StickerLottieSize::InlineResults, - st::giftBoxStickerSize, + stickerSize(), Lottie::Quality::High)); } else if (sticker->isWebm()) { result = std::make_unique( media->owner()->location(), media->bytes(), - st::giftBoxStickerSize); + stickerSize()); } else { result = std::make_unique( media->owner()->location(), media->bytes(), - st::giftBoxStickerSize); + stickerSize()); } result->setRepaintCallback([=] { update(); }); _playerDocument = media->owner(); @@ -440,7 +443,7 @@ QMargins GiftButton::currentExtend() const { } bool GiftButton::small() const { - return _mode != GiftButtonMode::Full; + return (_mode != Mode::Full) && (_mode != Mode::CraftResale); } void GiftButton::toggleSelected( @@ -595,6 +598,24 @@ rpl::producer GiftButton::mouseEvents() { return _mouseEvents.events(); } +bool GiftButton::makeCraftFrameIsFinal( + QImage &frame, + float64 progress) { + const auto ratio = style::DevicePixelRatio(); + if (frame.size() != size() * ratio) { + frame = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied); + frame.setDevicePixelRatio(ratio); + } + if (progress < 1.) { + frame.fill(Qt::transparent); + } + auto p = QPainter(&frame); + paint(p, progress); + return (progress == 1.) + && (!_uniquePatternEmoji || _uniquePatternEmoji->ready()) + && (!_player || (_player->ready() && _playerFinished)); +} + void GiftButton::cacheUniqueBackground( not_null unique, int width, @@ -620,34 +641,87 @@ void GiftButton::cacheUniqueBackground( const auto radius = st::giftBoxGiftRadius; auto p = QPainter(&_uniqueBackgroundCache); - auto hq = PainterHighQualityEnabler(p); - auto gradient = QRadialGradient(inner.center(), inner.width() / 2); - gradient.setStops({ - { 0., unique->backdrop.centerColor }, - { 1., unique->backdrop.edgeColor }, - }); - p.setBrush(gradient); - p.setPen(Qt::NoPen); - p.drawRoundedRect(inner, radius, radius); + paintUniqueBackgroundGradient(p, unique, inner, radius); _patterned = false; } if (!_patterned && _uniquePatternEmoji->ready()) { _patterned = true; auto p = QPainter(&_uniqueBackgroundCache); - p.setClipRect(inner); - const auto skip = inner.width() / 3; - Ui::PaintBgPoints( - p, - Ui::PatternBgPointsSmall(), - _uniquePatternCache, - _uniquePatternEmoji.get(), - *unique, - QRect(-skip, 0, inner.width() + 2 * skip, inner.height())); + paintUniqueBackgroundPattern(p, unique, inner); } } +void GiftButton::paintUniqueBackgroundGradient( + QPainter &p, + not_null unique, + QRect inner, + float64 radius) { + auto hq = PainterHighQualityEnabler(p); + auto gradient = QRadialGradient(inner.center(), inner.width() / 2); + gradient.setStops({ + { 0., unique->backdrop.centerColor }, + { 1., unique->backdrop.edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRoundedRect(inner, radius, radius); +} + +void GiftButton::paintUniqueBackgroundPattern( + QPainter &p, + not_null unique, + QRect inner) { + p.setClipRect(inner); + const auto skip = inner.width() / 3; + Ui::PaintBgPoints( + p, + Ui::PatternBgPointsSmall(), + _uniquePatternCache, + _uniquePatternEmoji.get(), + *unique, + QRect( + inner.x() - skip, + inner.y(), + inner.width() + 2 * skip, + inner.height())); +} + void GiftButton::paintEvent(QPaintEvent *e) { + const auto canCraftAt = [&] { + if (_mode != Mode::Craft) { + return 0; + } + const auto stargift = std::get_if(&_descriptor); + const auto unique = stargift ? stargift->info.unique.get() : nullptr; + return unique ? unique->canCraftAt : TimeId(); + }(); + auto p = QPainter(this); + if (!canCraftAt || base::unixtime::now() >= canCraftAt) { + paint(p); + return; + } + const auto ratio = style::DevicePixelRatio(); + const auto w = width() * ratio; + const auto h = height() * ratio; + auto cache = _delegate->craftUnavailableFrameCache(this, canCraftAt); + if (cache.width() < w || cache.height() < h) { + cache = QImage( + std::max(w, cache.width()), + std::max(h, cache.height()), + QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(ratio); + } + cache.fill(Qt::transparent); + auto q = QPainter(&cache); + paint(q); + q.end(); + + p.setOpacity(kCraftUnavailableOpacity); + p.drawImage(rect(), cache, QRect(0, 0, w, h)); +} + +void GiftButton::paint(QPainter &p, float64 craftProgress) { const auto stargift = std::get_if(&_descriptor); const auto unique = stargift ? stargift->info.unique.get() : nullptr; const auto onsale = unique && unique->starsForResale && small(); @@ -669,12 +743,36 @@ void GiftButton::paintEvent(QPaintEvent *e) { const auto extend = currentExtend(); const auto position = QPoint(extend.left(), extend.top()); const auto background = _delegate->background(); - const auto width = this->width(); const auto dpr = int(background.devicePixelRatio()); - paintBackground(p, background); + const auto width = this->width(); + const auto height = background.height() / dpr; + const auto roundedRatio = (1. - craftProgress); + if (craftProgress == 0.) { + paintBackground(p, background); + } else if (craftProgress < 1.) { + p.setOpacity(roundedRatio); + paintBackground(p, background); + p.setOpacity(1.); + } + if (unique) { - cacheUniqueBackground(unique, width, background.height() / dpr); - p.drawImage(extend.left(), extend.top(), _uniqueBackgroundCache); + if (craftProgress > 0.) { + const auto outer = QRect(0, 0, width, height); + const auto inner = outer.marginsRemoved(extend * roundedRatio); + paintUniqueBackgroundGradient( + p, + unique, + inner, + int(base::SafeRound(st::giftBoxGiftRadius * roundedRatio))); + + if (_uniquePatternEmoji->ready()) { + paintUniqueBackgroundPattern(p, unique, inner); + p.setClipping(false); + } + } else { + cacheUniqueBackground(unique, width, height); + p.drawImage(extend.left(), extend.top(), _uniqueBackgroundCache); + } } else if (requirePremium || auction) { auto hq = PainterHighQualityEnabler(p); auto pen = st::creditsFg->p; @@ -696,7 +794,6 @@ void GiftButton::paintEvent(QPaintEvent *e) { pen.setWidthF(progress * thickness); p.setPen(pen); p.setBrush(Qt::NoBrush); - const auto height = background.height() / dpr; const auto outer = QRectF(0, 0, width, height); const auto shift = progress * thickness * 2; const auto extend = QMarginsF(currentExtend()) @@ -734,26 +831,36 @@ void GiftButton::paintEvent(QPaintEvent *e) { auto frame = QImage(); if (_player && _player->ready()) { - const auto paused = !isOver(); + const auto paused = !isOver() || isHidden(); auto info = _player->frame( - st::giftBoxStickerSize, + stickerSize(), QColor(0, 0, 0, 0), false, crl::now(), paused); frame = info.image; - const auto finished = (info.index + 1 == _player->framesCount()); - if (!finished || !paused) { + _playerFinished = (info.index + 1 == _player->framesCount()); + if (!_playerFinished || !paused) { _player->markFrameShown(); } const auto size = frame.size() / style::DevicePixelRatio(); p.drawImage( QRect( (width - size.width()) / 2, - (small() + ((_mode == Mode::CraftPreview + || _mode == Mode::Minimal + || _mode == Mode::Craft) + ? (extend.top() + + ((height + - extend.top() + - extend.bottom() + - size.height()) / 2)) + : small() ? st::giftBoxSmallStickerTop : _text.isEmpty() - ? st::giftBoxStickerStarTop + ? (unique + ? st::giftBoxStickerUniqueTop + : st::giftBoxStickerStarTop) : _byStars.isEmpty() ? st::giftBoxStickerTop : st::giftBoxStickerTopByStars), @@ -763,11 +870,13 @@ void GiftButton::paintEvent(QPaintEvent *e) { } if (hidden) { const auto topleft = QPoint( - (width - st::giftBoxStickerSize.width()) / 2, + (width - stickerSize().width()) / 2, (small() ? st::giftBoxSmallStickerTop : _text.isEmpty() - ? st::giftBoxStickerStarTop + ? (unique + ? st::giftBoxStickerUniqueTop + : st::giftBoxStickerStarTop) : _byStars.isEmpty() ? st::giftBoxStickerTop : st::giftBoxStickerTopByStars)); @@ -776,7 +885,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { frame, _hiddenBgCache, topleft, - st::giftBoxStickerSize, + stickerSize(), width); } @@ -818,8 +927,8 @@ void GiftButton::paintEvent(QPaintEvent *e) { return GiftBadge{ .text = (onsale ? tr::lng_gift_stars_on_sale(tr::now) - : (unique && (data.resale || pinned)) - ? ('#' + QString::number(unique->number)) + : (unique && (data.resale || pinned || data.mine)) + ? ('#' + Lang::FormatCountDecimal(unique->number)) : data.resale ? tr::lng_gift_stars_resale(tr::now) : soldOut @@ -891,10 +1000,11 @@ void GiftButton::paintEvent(QPaintEvent *e) { cached); } + auto percentSkip = 0; v::match(_descriptor, [](const GiftTypePremium &) { }, [&](const GiftTypeStars &data) { - if (!unique) { - } else if (data.pinned && _mode != GiftButtonMode::Selection) { + if (!unique || _mode == Mode::Craft || _mode == Mode::CraftPreview) { + } else if (data.pinned && _mode != Mode::Selection) { auto hq = PainterHighQualityEnabler(p); const auto &icon = st::giftBoxPinIcon; const auto skip = st::giftBoxUserpicSkip; @@ -909,6 +1019,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { } else if (!data.forceTon && unique->nanoTonForResale && unique->onlyAcceptTon) { + auto hq = PainterHighQualityEnabler(p); if (_tonIcon.isNull()) { _tonIcon = st::tonIconEmojiLarge.icon.instance( QColor(255, 255, 255)); @@ -926,9 +1037,35 @@ void GiftButton::paintEvent(QPaintEvent *e) { extend.left() + skip + add, extend.top() + skip + add, _tonIcon); + percentSkip += st::giftBoxUserpicSize; } }); + if (unique + && unique->craftChancePermille > 0 + && (_mode == Mode::Craft || _mode == Mode::CraftResale)) { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(unique->backdrop.patternColor); + + const auto rounded = (unique->craftChancePermille + 5) / 10; + const auto percent = QString::number(rounded) + '%'; + const auto &font = st::giftBoxGiftBadgeFont; + const auto height = font->height + st::lineWidth; + const auto radius = height / 2.; + const auto skip = st::giftBoxUserpicSkip + + (_selected ? inset : st::giftBoxUserpicSkip); + const auto space = font->spacew; + const auto x = extend.left() + skip + percentSkip; + const auto y = extend.top() + skip; + const auto width = font->width(percent) + 2 * space; + p.drawRoundedRect(x, y, width, height, radius, radius); + + p.setPen(st::white); + p.setFont(font); + p.drawText(x + space, y + font->ascent, percent); + } + if (!_button.isEmpty()) { p.setBrush(onsale ? QBrush(unique->backdrop.patternColor) @@ -999,6 +1136,12 @@ void GiftButton::paintEvent(QPaintEvent *e) { } } +QSize GiftButton::stickerSize() const { + return (_mode == Mode::CraftPreview) + ? st::giftBoxStickerTiny + : st::giftBoxStickerSize; +} + Delegate::Delegate(not_null session, GiftButtonMode mode) : _session(session) , _hiddenMark(std::make_unique( @@ -1045,10 +1188,16 @@ QSize Delegate::buttonSize() { const auto available = width - padding.left() - padding.right(); const auto singlew = (available - 2 * st::giftBoxGiftSkip.x()) / kGiftsPerRow; - const auto minimal = (_mode != GiftButtonMode::Full); + const auto minimal = (_mode != GiftButtonMode::Full) + && (_mode != GiftButtonMode::CraftResale); + const auto tiny = (_mode == GiftButtonMode::CraftPreview); _single = QSize( - singlew, - minimal ? st::giftBoxGiftSmall : st::giftBoxGiftHeight); + tiny ? st::giftBoxGiftTiny : singlew, + (tiny + ? st::giftBoxGiftTiny + : minimal + ? st::giftBoxGiftSmall + : st::giftBoxGiftHeight)); return _single; } @@ -1127,10 +1276,7 @@ not_null Delegate::hiddenMark() { QImage Delegate::cachedBadge(const GiftBadge &badge) { auto &image = _badges[badge]; if (image.isNull()) { - const auto &extend = buttonExtend(); - const auto line = st::lineWidth; - const auto padding = QMargins(extend.top(), 0, extend.top(), line); - image = ValidateRotatedBadge(badge, padding); + image = ValidateRotatedBadge(badge, QMargins()); } return image; } @@ -1144,6 +1290,43 @@ void Delegate::invalidateCache() { _badges.clear(); } +QImage &Delegate::craftUnavailableFrameCache( + not_null button, + TimeId until) { + const auto weak = QPointer(button.get()); + if (!ranges::contains(_craftUnavailables, weak)) { + _craftUnavailables.push_back(weak); + } + if (!_craftUnavailableTimer) { + _craftUnavailableTimer = std::make_unique( + [=] { updateCraftUnavailables(); }); + } + if (!_craftUnavailableTimer->isActive() + || _craftUnavailableUntil > until) { + _craftUnavailableUntil = until; + const auto left = until - base::unixtime::now(); + _craftUnavailableTimer->callOnce( + std::clamp(left, 1, 86400) * crl::time(1000)); + } + return _craftUnavailableFrameCache; +} + +void Delegate::updateCraftUnavailables() { + Expects(_craftUnavailableTimer != nullptr); + + _craftUnavailableTimer->cancel(); + _craftUnavailableUntil = 0; + + for (auto i = begin(_craftUnavailables); i != end(_craftUnavailables);) { + if (const auto raw = i->data()) { + raw->update(); + ++i; + } else { + i = _craftUnavailables.erase(i); + } + } +} + DocumentData *LookupGiftSticker( not_null session, const GiftDescriptor &descriptor) { @@ -1181,7 +1364,10 @@ rpl::producer> GiftStickerValue( }); } -QImage ValidateRotatedBadge(const GiftBadge &badge, QMargins padding) { +QImage ValidateRotatedBadge( + const GiftBadge &badge, + QMargins padding, + bool left) { const auto &font = badge.small ? st::giftBoxGiftBadgeFont : st::msgServiceGiftBoxBadgeFont; @@ -1193,7 +1379,10 @@ QImage ValidateRotatedBadge(const GiftBadge &badge, QMargins padding) { const auto multiplier = ratio * 3; const auto size = (twidth + font->height * 2); const auto height = padding.top() + font->height + padding.bottom(); - const auto textpos = QPoint(size - skip, padding.top()); + const auto angle = left ? -45. : 45.; + const auto textpos = left + ? QPoint(padding.top(), size - skip) + : QPoint(size - skip, padding.top()); auto image = QImage( QSize(size, size) * multiplier, QImage::Format_ARGB32_Premultiplied); @@ -1203,7 +1392,7 @@ QImage ValidateRotatedBadge(const GiftBadge &badge, QMargins padding) { auto p = QPainter(&image); auto hq = PainterHighQualityEnabler(p); p.translate(textpos); - p.rotate(45.); + p.rotate(angle); p.setFont(font); p.setPen(badge.fg); p.drawText( @@ -1228,7 +1417,7 @@ QImage ValidateRotatedBadge(const GiftBadge &badge, QMargins padding) { p.save(); p.translate(textpos); - p.rotate(45.); + p.rotate(angle); const auto rect = QRect(-5 * twidth, 0, twidth * 12, height); if (badge.border.alpha() > 0) { p.setPen(badge.border); @@ -1404,4 +1593,8 @@ void SelectGiftToUnpin( })); } +QColor BurnedBadgeBg() { + return QColor(0xd0, 0x3a, 0x3b); +} + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index 9986e81912..0b31fd321e 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -138,8 +138,11 @@ struct GiftBadge { enum class GiftButtonMode : uint8 { Full, + Craft, + CraftResale, Minimal, Selection, + CraftPreview, }; enum class GiftSelectionMode : uint8 { @@ -148,6 +151,8 @@ enum class GiftSelectionMode : uint8 { Check, }; +class GiftButton; + class GiftButtonDelegate { public: [[nodiscard]] virtual TextWithEntities star() = 0; @@ -168,6 +173,10 @@ public: [[nodiscard]] virtual QImage cachedBadge(const GiftBadge &badge) = 0; [[nodiscard]] virtual bool amPremium() = 0; virtual void invalidateCache() = 0; + [[nodiscard]] virtual QImage &craftUnavailableFrameCache( + not_null button, + TimeId until) = 0; + }; class GiftButton final : public Ui::AbstractButton { @@ -187,6 +196,10 @@ public: [[nodiscard]] rpl::producer contextMenuRequests() const; [[nodiscard]] rpl::producer mouseEvents(); + [[nodiscard]] bool makeCraftFrameIsFinal( + QImage &frame, + float64 progress); + private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; @@ -195,14 +208,25 @@ private: void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; + void paint(QPainter &p, float64 craftProgress = 0.); void paintBackground(QPainter &p, const QImage &background); void cacheUniqueBackground( not_null unique, int width, int height); + void paintUniqueBackgroundGradient( + QPainter &p, + not_null unique, + QRect inner, + float64 radius); + void paintUniqueBackgroundPattern( + QPainter &p, + not_null unique, + QRect inner); void refreshLocked(); void setDocument(not_null document); + [[nodiscard]] QSize stickerSize() const; [[nodiscard]] QMargins currentExtend() const; [[nodiscard]] bool small() const; @@ -232,6 +256,7 @@ private: bool _patterned : 1 = false; bool _selected : 1 = false; bool _locked : 1 = false; + bool _playerFinished : 1 = false; bool _mouseEventsAreListening = false; @@ -274,8 +299,13 @@ public: QImage cachedBadge(const GiftBadge &badge) override; bool amPremium() override; void invalidateCache() override; + QImage &craftUnavailableFrameCache( + not_null button, + TimeId until) override; private: + void updateCraftUnavailables(); + const not_null _session; std::unique_ptr _hiddenMark; base::flat_map _badges; @@ -286,6 +316,11 @@ private: TextWithEntities _ministarEmoji; TextWithEntities _starEmoji; + QImage _craftUnavailableFrameCache; + std::vector> _craftUnavailables; + std::unique_ptr _craftUnavailableTimer; + TimeId _craftUnavailableUntil = 0; + }; [[nodiscard]] DocumentData *LookupGiftSticker( @@ -298,11 +333,14 @@ private: [[nodiscard]] QImage ValidateRotatedBadge( const GiftBadge &badge, - QMargins padding); + QMargins padding, + bool left = false); void SelectGiftToUnpin( std::shared_ptr show, const std::vector &pinned, Fn chosen); +[[nodiscard]] QColor BurnedBadgeBg(); + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 198c78000d..8e1d863418 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -200,6 +200,7 @@ private: void subscribeToUpdates(); void applyUpdateTo(Entries &entries, const Data::GiftUpdate &update); + void switchTo(int collectionId); void loadCollections(); void loadMore(); void loaded(const MTPpayments_SavedStarGifts &result); @@ -390,17 +391,20 @@ InnerWidget::InnerWidget( _descriptor.value( ) | rpl::on_next([=](Descriptor now) { - const auto id = now.collectionId; - _collectionsLoadedCallback = nullptr; - _api.request(base::take(_loadMoreRequestId)).cancel(); - _entries = id ? &_perCollection[id] : &_all; - _list = &_entries->list; - refreshButtons(); - refreshAbout(); - loadMore(); + switchTo(now.collectionId); }, lifetime()); } +void InnerWidget::switchTo(int collectionId) { + _collectionsLoadedCallback = nullptr; + _api.request(base::take(_loadMoreRequestId)).cancel(); + _entries = collectionId ? &_perCollection[collectionId] : &_all; + _list = &_entries->list; + refreshButtons(); + refreshAbout(); + loadMore(); +} + void InnerWidget::loadCollections() { if (_addingToCollectionId) { return; @@ -433,7 +437,9 @@ void InnerWidget::subscribeToUpdates() { ) | rpl::on_next([=](const Data::GiftUpdate &update) { applyUpdateTo(_all, update); using Action = Data::GiftUpdate::Action; - if (update.action == Action::Pin || update.action == Action::Unpin) { + if (update.action == Action::Pin + || update.action == Action::Unpin + || update.action == Action::Delete) { for (auto &[_, entries] : _perCollection) { applyUpdateTo(entries, update); } @@ -504,6 +510,9 @@ void InnerWidget::applyUpdateTo( view.manageId = {}; } } + } else if (update.action == Action::Upgraded) { + _scrollToTop.fire({}); + reloadCollection(_descriptor.current().collectionId); } else { return; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index a3b7ddb5c7..c036f3935a 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -56,7 +56,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/channel_statistics/earn/earn_format.h" #include "info/channel_statistics/earn/earn_icons.h" #include "info/channel_statistics/earn/info_channel_earn_list.h" -#include "info/profile/info_profile_cover.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_phone_menu.h" #include "info/profile/info_profile_text.h" @@ -2006,12 +2005,14 @@ object_ptr DetailsFiller::setupPersonalChannel( ) | rpl::map([](TextWithEntities &&text, ChannelData *channel) { const auto count = channel ? channel->membersCount() : 0; if (count > 1) { - text.append( - QString::fromUtf8(" \xE2\x80\xA2 ") - ).append(tr::lng_chat_status_subscribers( - tr::now, - lt_count_decimal, - count)); + text.append(' ') + .append(Ui::kQBullet) + .append(' ') + .append( + tr::lng_chat_status_subscribers( + tr::now, + lt_count_decimal, + count)); } return text; }); @@ -2836,7 +2837,7 @@ void ActionsFiller::addBotCommandActions(not_null user) { tr::lng_profile_bot_privacy(), rpl::single(true), openPrivacyPolicy, - nullptr); + &st::infoIconPrivacyPolicy); } void ActionsFiller::addReportAction() { @@ -3284,33 +3285,6 @@ object_ptr SetupChannelMembersAndManage( return result; } -Cover *AddCover( - not_null container, - not_null controller, - not_null peer, - Data::ForumTopic *topic, - Data::SavedSublist *sublist) { - const auto shown = sublist ? sublist->sublistPeer() : peer; - const auto result = topic - ? container->add(object_ptr( - container, - controller->parentController(), - topic)) - : container->add(object_ptr( - container, - controller->parentController(), - shown, - [=] { return controller->wrapWidget(); })); - result->showSection( - ) | rpl::on_next([=](Section section) { - controller->showSection(topic - ? std::make_shared(topic, section) - : std::make_shared(shown, section)); - }, result->lifetime()); - result->setOnlineCount(rpl::single(0)); - return result; -} - void AddDetails( not_null container, not_null controller, diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.h b/Telegram/SourceFiles/info/profile/info_profile_actions.h index 46794b328d..51738ba9d8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.h +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.h @@ -29,7 +29,6 @@ namespace Info::Profile { extern const char kOptionShowPeerIdBelowAbout[]; extern const char kOptionShowChannelJoinedBelowAbout[]; -class Cover; struct Origin; object_ptr SetupDetails( @@ -61,12 +60,6 @@ object_ptr SetupChannelMembersAndManage( not_null parent, not_null peer); -Cover *AddCover( - not_null container, - not_null controller, - not_null peer, - Data::ForumTopic *topic, - Data::SavedSublist *sublist); void AddDetails( not_null container, not_null controller, diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 0268271b6d..3aa24cd0dc 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -7,55 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/profile/info_profile_cover.h" -#include "api/api_user_privacy.h" -#include "base/timer_rpl.h" -#include "data/data_peer_values.h" -#include "data/data_channel.h" -#include "data/data_chat.h" -#include "data/data_emoji_statuses.h" -#include "data/data_peer.h" -#include "data/data_user.h" +#include "main/main_session.h" +#include "data/data_forum_topic.h" #include "data/data_document.h" #include "data/data_document_media.h" -#include "data/data_changes.h" #include "data/data_session.h" -#include "data/data_forum_topic.h" #include "data/stickers/data_custom_emoji.h" -#include "info/profile/info_profile_badge.h" -#include "info/profile/info_profile_badge_tooltip.h" -#include "info/profile/info_profile_emoji_status_panel.h" -#include "info/profile/info_profile_status_label.h" -#include "info/profile/info_profile_values.h" -#include "info/info_controller.h" -#include "info/info_memento.h" -#include "boxes/peers/edit_forum_topic_box.h" -#include "boxes/report_messages_box.h" #include "history/view/media/history_view_sticker_player.h" -#include "lang/lang_keys.h" -#include "ui/boxes/show_or_premium_box.h" -#include "ui/controls/stars_rating.h" -#include "ui/controls/userpic_button.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/popup_menu.h" -#include "ui/text/text_utilities.h" -#include "ui/basic_click_handlers.h" -#include "ui/ui_utility.h" -#include "ui/painter.h" -#include "base/event_filter.h" -#include "base/unixtime.h" -#include "window/window_controller.h" -#include "window/window_session_controller.h" -#include "main/main_app_config.h" -#include "main/main_session.h" -#include "settings/settings_premium.h" +#include "info/profile/info_profile_values.h" #include "chat_helpers/stickers_lottie.h" -#include "apiwrap.h" -#include "api/api_peer_photo.h" -#include "styles/style_boxes.h" +#include "window/window_session_controller.h" +#include "ui/painter.h" #include "styles/style_info.h" #include "styles/style_dialogs.h" -#include "styles/style_menu_icons.h" // AyuGram includes #include "ayu/ayu_settings.h" @@ -66,27 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Profile { -namespace { - -constexpr auto kWaitBeforeGiftBadge = crl::time(1000); -constexpr auto kGiftBadgeGlares = 3; -constexpr auto kGlareDurationStep = crl::time(320); -constexpr auto kGlareTimeout = crl::time(1000); - -[[nodiscard]] const style::InfoProfileCover &CoverStyle( - not_null peer, - Data::ForumTopic *topic, - Cover::Role role) { - return (role == Cover::Role::EditContact) - ? st::infoEditContactCover - : topic - ? st::infoTopicCover - : peer->isMegagroup() - ? st::infoProfileMegagroupCover - : st::infoProfileCover; -} - -} // namespace QMargins LargeCustomEmojiMargins() { const auto ratio = style::DevicePixelRatio(); @@ -121,7 +64,7 @@ TopicIconView::TopicIconView( setup(topic); } -void TopicIconView::paintInRect(QPainter &p, QRect rect) { +void TopicIconView::paintInRect(QPainter &p, QRect rect, QColor textColor) { const auto paint = [&](const QImage &image) { const auto size = image.size() / style::DevicePixelRatio(); p.drawImage( @@ -133,7 +76,9 @@ void TopicIconView::paintInRect(QPainter &p, QRect rect) { image); }; if (_player && _player->ready()) { - const auto colored = _playerUsesTextColor + const auto colored = (textColor.alpha() > 0) + ? textColor + : _playerUsesTextColor ? st::windowFg->c : QColor(0, 0, 0, 0); paint(_player->frame( @@ -260,638 +205,4 @@ TopicIconButton::TopicIconButton( }, lifetime()); } -Cover::Cover( - QWidget *parent, - not_null controller, - not_null peer, - Fn()> parentForTooltip) -: Cover( - parent, - controller, - peer, - nullptr, - Role::Info, - NameValue(peer), - parentForTooltip) { -} - -Cover::Cover( - QWidget *parent, - not_null controller, - not_null topic) -: Cover( - parent, - controller, - topic->peer(), - topic, - Role::Info, - TitleValue(topic), - nullptr) { -} - -Cover::Cover( - QWidget *parent, - not_null controller, - not_null peer, - Role role, - rpl::producer title) -: Cover( - parent, - controller, - peer, - nullptr, - role, - std::move(title), - nullptr) { -} - -Cover::Cover( - QWidget *parent, - not_null controller, - not_null peer, - Data::ForumTopic *topic, - Role role, - rpl::producer title, - Fn()> parentForTooltip) -: FixedHeightWidget(parent, CoverStyle(peer, topic, role).height) -, _st(CoverStyle(peer, topic, role)) -, _role(role) -, _controller(controller) -, _peer(peer) -, _emojiStatusPanel(peer->isSelf() - ? std::make_unique() - : nullptr) -, _botVerify(role == Role::EditContact - ? nullptr - : std::make_unique( - this, - st::infoBotVerifyBadge, - &peer->session(), - BotVerifyBadgeForPeer(peer), - nullptr, - [=] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); - })) -, _badgeContent(BadgeContentForPeer(peer)) -, _badge(role == Role::EditContact - ? nullptr - : std::make_unique( - this, - st::infoPeerBadge, - &peer->session(), - _badgeContent.value(), - _emojiStatusPanel.get(), - [=] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); - })) -, _verified(role == Role::EditContact - ? nullptr - : std::make_unique( - this, - st::infoPeerBadge, - &peer->session(), - VerifiedContentForPeer(peer), - _emojiStatusPanel.get(), - [=] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); - })) -, _exteraBadge(role == Role::EditContact - ? nullptr - : std::make_unique( - this, - st::infoPeerBadge, - &peer->session(), - ExteraBadgeTypeFromPeer(peer), - _emojiStatusPanel.get(), - [=] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); - })) -, _parentForTooltip(std::move(parentForTooltip)) -, _badgeTooltipHide([=] { hideBadgeTooltip(); }) -, _userpic(topic - ? nullptr - : object_ptr( - this, - controller, - _peer->userpicPaintingPeer(), - Ui::UserpicButton::Role::OpenPhoto, - Ui::UserpicButton::Source::PeerPhoto, - _st.photo, - _peer->userpicShape())) -, _changePersonal((role == Role::Info - || role == Role::EditContact - || topic - || !_peer->isUser() - || _peer->isSelf() - || _peer->asUser()->isBot()) - ? nullptr - : CreateUploadSubButton(this, _peer->asUser(), controller).get()) -, _iconButton(topic - ? object_ptr(this, controller, topic) - : nullptr) -, _name(this, _st.name) -, _starsRating(_peer->isUser() && _role != Role::EditContact - ? std::make_unique( - this, - _controller->uiShow(), - _peer->isSelf() ? QString() : _peer->shortName(), - Data::StarsRatingValue(_peer), - (_peer->isSelf() - ? [=] { return _peer->owner().pendingStarsRating(); } - : Fn())) - : nullptr) -, _status(this, _st.status) -, _statusLabel(std::make_unique(_status.data(), _peer)) -, _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) { - _peer->updateFull(); - if (const auto broadcast = _peer->monoforumBroadcast()) { - broadcast->updateFull(); - } - - _name->setSelectable(true); - _name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now)); - - if (!_peer->isMegagroup()) { - _status->setAttribute(Qt::WA_TransparentForMouseEvents); - if (const auto rating = _starsRating.get()) { - _statusShift = rating->widthValue(); - _statusShift.changes() | rpl::on_next([=] { - refreshStatusGeometry(width()); - }, _status->lifetime()); - rating->raise(); - } - } - - setupShowLastSeen(); - - if (_badge) { - _badge->setPremiumClickCallback([=] { - if (const auto panel = _emojiStatusPanel.get()) { - panel->show(_controller, _badge->widget(), _badge->sizeTag()); - } else { - ::Settings::ShowEmojiStatusPremium(_controller, _peer); - } - }); - } - auto badgeUpdates = rpl::producer(); - if (_badge) { - badgeUpdates = rpl::merge( - std::move(badgeUpdates), - _badge->updated()); - } - if (_verified) { - badgeUpdates = rpl::merge( - std::move(badgeUpdates), - _verified->updated()); - } - if (_botVerify) { - badgeUpdates = rpl::merge( - std::move(badgeUpdates), - _botVerify->updated()); - } - if (_exteraBadge) { - const auto isCustomBadge = isCustomBadgePeer(getBareID(_peer)); - const auto isExtera = isExteraPeer(getBareID(_peer)); - const auto isSupporter = isSupporterPeer(getBareID(_peer)); - if (isExtera || isSupporter || isCustomBadge) { - _exteraBadge->setPremiumClickCallback(badgeClickHandler(_peer)); - } - badgeUpdates = rpl::merge( - std::move(badgeUpdates), - _exteraBadge->updated()); - } - std::move(badgeUpdates) | rpl::on_next([=] { - refreshNameGeometry(width()); - }, _name->lifetime()); - - initViewers(std::move(title)); - setupChildGeometry(); - setupUniqueBadgeTooltip(); - - if (_userpic) { - } else if (topic->canEdit()) { - _iconButton->setClickedCallback([=] { - _controller->show(Box( - EditForumTopicBox, - _controller, - topic->history(), - topic->rootId())); - }); - } else { - _iconButton->setAttribute(Qt::WA_TransparentForMouseEvents); - } -} - -void Cover::setupShowLastSeen() { - const auto user = _peer->asUser(); - if (_st.showLastSeenVisible - && user - && !user->isSelf() - && !user->isBot() - && !user->isServiceUser() - && user->session().premiumPossible()) { - if (user->session().premium()) { - if (user->lastseen().isHiddenByMe()) { - user->updateFullForced(); - } - _showLastSeen->hide(); - return; - } - - rpl::combine( - user->session().changes().peerFlagsValue( - user, - Data::PeerUpdate::Flag::OnlineStatus), - Data::AmPremiumValue(&user->session()) - ) | rpl::on_next([=](auto, bool premium) { - const auto wasShown = !_showLastSeen->isHidden(); - const auto hiddenByMe = user->lastseen().isHiddenByMe(); - const auto shown = hiddenByMe - && !user->lastseen().isOnline(base::unixtime::now()) - && !premium - && user->session().premiumPossible(); - _showLastSeen->setVisible(shown); - if (wasShown && premium && hiddenByMe) { - user->updateFullForced(); - } - }, _showLastSeen->lifetime()); - - _controller->session().api().userPrivacy().value( - Api::UserPrivacy::Key::LastSeen - ) | rpl::filter([=](Api::UserPrivacy::Rule rule) { - return (rule.option == Api::UserPrivacy::Option::Everyone); - }) | rpl::on_next([=] { - if (user->lastseen().isHiddenByMe()) { - user->updateFullForced(); - } - }, _showLastSeen->lifetime()); - } else { - _showLastSeen->hide(); - } - - using TextTransform = Ui::RoundButton::TextTransform; - _showLastSeen->setTextTransform(TextTransform::NoTransform); - _showLastSeen->setFullRadius(true); - - _showLastSeen->setClickedCallback([=] { - const auto type = Ui::ShowOrPremium::LastSeen; - auto box = Box(Ui::ShowOrPremiumBox, type, user->shortName(), [=] { - _controller->session().api().userPrivacy().save( - ::Api::UserPrivacy::Key::LastSeen, - {}); - }, [=] { - ::Settings::ShowPremium(_controller, u"lastseen_hidden"_q); - }); - _controller->show(std::move(box)); - }); -} - -void Cover::setupChildGeometry() { - widthValue( - ) | rpl::on_next([this](int newWidth) { - if (_userpic) { - _userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth); - } else { - _iconButton->moveToLeft(_st.photoLeft, _st.photoTop, newWidth); - } - if (_changePersonal) { - _changePersonal->moveToLeft( - (_st.photoLeft - + _st.photo.photoSize - - _changePersonal->width() - + st::infoEditContactPersonalLeft), - (_userpic->y() - + _userpic->height() - - _changePersonal->height())); - } - refreshNameGeometry(newWidth); - refreshStatusGeometry(newWidth); - }, lifetime()); -} - -Cover *Cover::setOnlineCount(rpl::producer &&count) { - std::move(count) | rpl::on_next([=](int value) { - if (_statusLabel) { - _statusLabel->setOnlineCount(value); - refreshStatusGeometry(width()); - } - }, lifetime()); - return this; -} - -std::optional Cover::updatedPersonalPhoto() const { - return _personalChosen; -} - -void Cover::initViewers(rpl::producer title) { - using Flag = Data::PeerUpdate::Flag; - std::move( - title - ) | rpl::on_next([=](const QString &title) { - _name->setText(title); - refreshNameGeometry(width()); - }, lifetime()); - - _statusLabel->setMembersLinkCallback([=] { - _showSection.fire(Section::Type::Members); - }); - - _peer->session().changes().peerFlagsValue( - _peer, - Flag::OnlineStatus | Flag::Members - ) | rpl::on_next([=] { - _statusLabel->refresh(); - refreshStatusGeometry(width()); - }, lifetime()); - - _peer->session().changes().peerFlagsValue( - _peer, - (_peer->isUser() ? Flag::IsContact : Flag::Rights) - ) | rpl::on_next([=] { - refreshUploadPhotoOverlay(); - }, lifetime()); - - setupChangePersonal(); -} - -void Cover::refreshUploadPhotoOverlay() { - if (!_userpic) { - return; - } else if (_role == Role::EditContact) { - _userpic->setAttribute(Qt::WA_TransparentForMouseEvents); - return; - } - - const auto canChange = [&] { - if (const auto chat = _peer->asChat()) { - return chat->canEditInformation(); - } else if (const auto channel = _peer->asChannel()) { - return channel->canEditInformation() - && !channel->isMonoforum(); - } else if (const auto user = _peer->asUser()) { - return user->isSelf() - || (user->isContact() - && !user->isInaccessible() - && !user->isServiceUser()); - } - Unexpected("Peer type in Info::Profile::Cover."); - }(); - - _userpic->switchChangePhotoOverlay(canChange, [=]( - Ui::UserpicButton::ChosenImage chosen) { - using ChosenType = Ui::UserpicButton::ChosenType; - auto result = Api::PeerPhoto::UserPhoto{ - base::take(chosen.image), // Strange MSVC bug with take. - chosen.markup.documentId, - chosen.markup.colors, - }; - switch (chosen.type) { - case ChosenType::Set: - _userpic->showCustom(base::duplicate(result.image)); - _peer->session().api().peerPhoto().upload( - _peer, - std::move(result)); - break; - case ChosenType::Suggest: - _peer->session().api().peerPhoto().suggest( - _peer, - std::move(result)); - break; - } - }); - - const auto canReport = [=, peer = _peer] { - if (!peer->hasUserpic()) { - return false; - } - const auto user = peer->asUser(); - if (!user) { - if (canChange) { - return false; - } - } else if (user->hasPersonalPhoto() - || user->isSelf() - || user->isInaccessible() - || user->isRepliesChat() - || user->isVerifyCodes() - || (user->botInfo && user->botInfo->canEditInformation) - || user->isServiceUser()) { - return false; - } - return true; - }; - - const auto contextMenu = _userpic->lifetime() - .make_state>(); - const auto showMenu = [=, peer = _peer, controller = _controller]( - not_null parent) { - if (!canReport()) { - return false; - } - *contextMenu = base::make_unique_q( - parent, - st::popupMenuWithIcons); - contextMenu->get()->addAction(tr::lng_profile_report(tr::now), [=] { - controller->show( - ReportProfilePhotoBox( - peer, - peer->owner().photo(peer->userpicPhotoId())), - Ui::LayerOption::CloseOther); - }, &st::menuIconReport); - contextMenu->get()->popup(QCursor::pos()); - return true; - }; - base::install_event_filter(_userpic, [showMenu, raw = _userpic.data()]( - not_null e) { - return (e->type() == QEvent::ContextMenu && showMenu(raw)) - ? base::EventFilterResult::Cancel - : base::EventFilterResult::Continue; - }); - - if (const auto user = _peer->asUser()) { - _userpic->resetPersonalRequests( - ) | rpl::on_next([=] { - user->session().api().peerPhoto().clearPersonal(user); - _userpic->showSource(Ui::UserpicButton::Source::PeerPhoto); - }, lifetime()); - } -} - -void Cover::setupChangePersonal() { - if (!_changePersonal) { - return; - } - - _changePersonal->chosenImages( - ) | rpl::on_next([=](Ui::UserpicButton::ChosenImage &&chosen) { - if (chosen.type == Ui::UserpicButton::ChosenType::Suggest) { - _peer->session().api().peerPhoto().suggest( - _peer, - { - std::move(chosen.image), - chosen.markup.documentId, - chosen.markup.colors, - }); - } else { - _personalChosen = std::move(chosen.image); - _userpic->showCustom(base::duplicate(*_personalChosen)); - _changePersonal->overrideHasPersonalPhoto(true); - _changePersonal->showSource( - Ui::UserpicButton::Source::NonPersonalIfHasPersonal); - } - }, _changePersonal->lifetime()); - - _changePersonal->resetPersonalRequests( - ) | rpl::on_next([=] { - _personalChosen = QImage(); - _userpic->showSource( - Ui::UserpicButton::Source::NonPersonalPhoto); - _changePersonal->overrideHasPersonalPhoto(false); - _changePersonal->showCustom(QImage()); - }, _changePersonal->lifetime()); -} - - - -Cover::~Cover() { - base::take(_badgeTooltip); - base::take(_badgeOldTooltips); -} - -void Cover::refreshNameGeometry(int newWidth) { - auto nameWidth = newWidth - _st.nameLeft - _st.rightSkip; - const auto verifiedWidget = _verified ? _verified->widget() : nullptr; - const auto badgeWidget = _badge ? _badge->widget() : nullptr; - const auto exteraWidget = _exteraBadge ? _exteraBadge->widget() : nullptr; - if (verifiedWidget) { - nameWidth -= verifiedWidget->width(); - } - if (badgeWidget) { - nameWidth -= badgeWidget->width(); - } - if (verifiedWidget || badgeWidget) { - nameWidth -= st::infoVerifiedCheckPosition.x(); - } - if (exteraWidget) { - nameWidth -= exteraWidget->width(); - nameWidth -= st::infoVerifiedCheckPosition.x(); - } - auto nameLeft = _st.nameLeft; - const auto badgeTop = _st.nameTop; - const auto badgeBottom = _st.nameTop + _name->height(); - const auto margins = LargeCustomEmojiMargins(); - - if (_botVerify) { - _botVerify->move(nameLeft - margins.left(), badgeTop, badgeBottom); - if (const auto widget = _botVerify->widget()) { - const auto skip = widget->width() - + st::infoVerifiedCheckPosition.x(); - nameLeft += skip; - nameWidth -= skip; - } - } - _name->resizeToNaturalWidth(nameWidth); - _name->moveToLeft(nameLeft, _st.nameTop, newWidth); - const auto badgeLeft = nameLeft + _name->width(); - if (_badge) { - _badge->move(badgeLeft, badgeTop, badgeBottom); - } - if (_verified) { - _verified->move( - badgeLeft + (badgeWidget ? badgeWidget->width() : 0), - badgeTop, - badgeBottom); - } - if (_exteraBadge) { - const auto exteraBadgeLeft = badgeLeft - + (badgeWidget ? badgeWidget->width() : 0) - + (badgeWidget && verifiedWidget ? st::infoVerifiedCheckPosition.x() : 0) - + (verifiedWidget ? verifiedWidget->width() : 0) - + ((badgeWidget || verifiedWidget) ? st::infoVerifiedCheckPosition.x() : 0); - const auto exteraBadgeTop = _st.nameTop; - const auto exteraBadgeBottom = _st.nameTop + _name->height(); - _exteraBadge->move(exteraBadgeLeft, exteraBadgeTop, exteraBadgeBottom); - } -} - -void Cover::refreshStatusGeometry(int newWidth) { - if (const auto rating = _starsRating.get()) { - rating->moveTo(_st.starsRatingLeft, _st.starsRatingTop); - } - const auto statusLeft = _st.statusLeft + _statusShift.current(); - auto statusWidth = newWidth - statusLeft - _st.rightSkip; - _status->resizeToNaturalWidth(statusWidth); - _status->moveToLeft(statusLeft, _st.statusTop, newWidth); - const auto left = statusLeft + _status->textMaxWidth(); - _showLastSeen->moveToLeft( - left + _st.showLastSeenPosition.x(), - _st.showLastSeenPosition.y(), - newWidth); -} - -void Cover::hideBadgeTooltip() { - _badgeTooltipHide.cancel(); - if (auto old = base::take(_badgeTooltip)) { - const auto raw = old.get(); - _badgeOldTooltips.push_back(std::move(old)); - - raw->fade(false); - raw->shownValue( - ) | rpl::filter( - !rpl::mappers::_1 - ) | rpl::on_next([=] { - const auto i = ranges::find( - _badgeOldTooltips, - raw, - &std::unique_ptr::get); - if (i != end(_badgeOldTooltips)) { - _badgeOldTooltips.erase(i); - } - }, raw->lifetime()); - } -} - -void Cover::setupUniqueBadgeTooltip() { - if (!_badge) { - return; - } - base::timer_once(kWaitBeforeGiftBadge) | rpl::then( - _badge->updated() - ) | rpl::on_next([=] { - const auto widget = _badge->widget(); - const auto &content = _badgeContent.current(); - const auto &collectible = content.emojiStatusId.collectible; - const auto premium = (content.badge == BadgeType::Premium); - const auto id = (collectible && widget && premium) - ? collectible->id - : uint64(); - if (_badgeCollectibleId == id) { - return; - } - hideBadgeTooltip(); - if (!collectible) { - return; - } - const auto parent = _parentForTooltip - ? _parentForTooltip() - : _controller->window().widget()->bodyWidget(); - _badgeTooltip = std::make_unique( - parent, - collectible, - widget); - const auto raw = _badgeTooltip.get(); - raw->fade(true); - _badgeTooltipHide.callOnce(kGiftBadgeGlares * raw->glarePeriod() - - st::infoGiftTooltip.duration * 1.5); - }, lifetime()); - - if (const auto raw = _badgeTooltip.get()) { - raw->finishAnimating(); - } -} - } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 2eab9d930a..3499809b04 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -7,24 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "info/profile/info_profile_badge.h" -#include "ui/wrap/padding_wrap.h" #include "ui/abstract_button.h" -#include "base/timer.h" namespace Window { class SessionController; } // namespace Window -namespace Ui { -class UserpicButton; -class FlatLabel; -template -class SlideWrap; -class RoundButton; -class StarsRating; -} // namespace Ui - namespace HistoryView { class StickerPlayer; } // namespace HistoryView @@ -33,22 +21,8 @@ namespace Data { class ForumTopic; } // namespace Data -namespace Info { -class Controller; -class Section; -} // namespace Info - -namespace style { -struct InfoProfileCover; -} // namespace style - namespace Info::Profile { -class BadgeTooltip; -class EmojiStatusPanel; -class Badge; -class StatusLabel; - [[nodiscard]] QMargins LargeCustomEmojiMargins(); class TopicIconView final { @@ -63,7 +37,10 @@ public: Fn update, const style::color &generalIconFg); - void paintInRect(QPainter &p, QRect rect); + void paintInRect( + QPainter &p, + QRect rect, + QColor textColor = QColor(0, 0, 0, 0)); private: using StickerPlayer = HistoryView::StickerPlayer; @@ -99,90 +76,4 @@ private: }; -class Cover final : public Ui::FixedHeightWidget { -public: - enum class Role { - Info, - EditContact, - }; - - Cover( - QWidget *parent, - not_null controller, - not_null peer, - Fn()> parentForTooltip = nullptr); - Cover( - QWidget *parent, - not_null controller, - not_null topic); - Cover( - QWidget *parent, - not_null controller, - not_null peer, - Role role, - rpl::producer title); - ~Cover(); - - Cover *setOnlineCount(rpl::producer &&count); - - [[nodiscard]] rpl::producer
showSection() const { - return _showSection.events(); - } - [[nodiscard]] std::optional updatedPersonalPhoto() const; - -private: - - Cover( - QWidget *parent, - not_null controller, - not_null peer, - Data::ForumTopic *topic, - Role role, - rpl::producer title, - Fn()> parentForTooltip); - - void setupShowLastSeen(); - void setupChildGeometry(); - void initViewers(rpl::producer title); - void refreshNameGeometry(int newWidth); - void refreshStatusGeometry(int newWidth); - void refreshUploadPhotoOverlay(); - void setupUniqueBadgeTooltip(); - void setupChangePersonal(); - void hideBadgeTooltip(); - - const style::InfoProfileCover &_st; - - const Role _role = Role::Info; - const not_null _controller; - const not_null _peer; - const std::unique_ptr _emojiStatusPanel; - const std::unique_ptr _botVerify; - rpl::variable _badgeContent; - const std::unique_ptr _badge; - const std::unique_ptr _verified; - const std::unique_ptr _exteraBadge; - - const Fn()> _parentForTooltip; - std::unique_ptr _badgeTooltip; - std::vector> _badgeOldTooltips; - base::Timer _badgeTooltipHide; - uint64 _badgeCollectibleId = 0; - - const object_ptr _userpic; - Ui::UserpicButton *_changePersonal = nullptr; - std::optional _personalChosen; - object_ptr _iconButton; - object_ptr _name = { nullptr }; - std::unique_ptr _starsRating; - object_ptr _status = { nullptr }; - std::unique_ptr _statusLabel; - rpl::variable _statusShift = 0; - object_ptr _showLastSeen = { nullptr }; - //object_ptr _dropArea = { nullptr }; - - rpl::event_stream
_showSection; - -}; - } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index 7f860acfeb..a8394e96bf 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -63,8 +63,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "menu/menu_mute.h" #include "settings/settings_credits_graphics.h" -#include "settings/settings_information.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_information.h" +#include "settings/sections/settings_premium.h" #include "ui/boxes/show_or_premium_box.h" #include "ui/color_contrast.h" #include "ui/controls/stars_rating.h" @@ -781,7 +781,8 @@ void TopBar::setupActions(not_null controller) { buttons.push_back(message); _actions->add(message); } - if (!topic && channel && !channel->amIn()) { + const auto canJoin = (!sublist && !topic && channel && !channel->amIn()); + if (canJoin) { const auto join = Ui::CreateChild( this, tr::lng_profile_action_short_join(tr::now), @@ -804,7 +805,7 @@ void TopBar::setupActions(not_null controller) { buttons.push_back(message); _actions->add(message); } - { + if (!peer->isSelf()) { const auto notifications = Ui::CreateChild( this, tr::lng_profile_action_short_mute(tr::now), @@ -887,7 +888,7 @@ void TopBar::setupActions(not_null controller) { tr::lng_profile_action_short_call(tr::now), st::infoProfileTopBarActionCall); call->setClickedCallback([=] { - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, {}); }); buttons.push_back(call); _actions->add(call); @@ -917,7 +918,7 @@ void TopBar::setupActions(not_null controller) { if (chechMax()) { return; } - if ((topic && topic->canEdit()) || EditPeerInfoBox::Available(peer)) { + if (topic ? topic->canEdit() : EditPeerInfoBox::Available(peer)) { const auto manage = Ui::CreateChild( this, tr::lng_profile_action_short_manage(tr::now), @@ -969,6 +970,7 @@ void TopBar::setupActions(not_null controller) { return; } if (!topic + && canJoin && ((chat && !chat->amCreator() && !chat->hasAdminRights()) || (channel && !channel->amCreator() @@ -987,16 +989,13 @@ void TopBar::setupActions(not_null controller) { if (chechMax()) { return; } - if (!topic && !sublist && channel && channel->amIn()) { + if (!canJoin && !topic && !sublist && (chat || channel)) { const auto leaveButton = Ui::CreateChild( this, tr::lng_profile_action_short_leave(tr::now), st::infoProfileTopBarActionLeave); - leaveButton->setClickedCallback([=] { - if (!controller->showFrozenError()) { - controller->show(Box(DeleteChatBox, peer)); - } - }); + leaveButton->setClickedCallback( + Window::DeleteAndLeaveHandler(controller, peer)); _actions->add(leaveButton); buttons.push_back(leaveButton); } @@ -1353,9 +1352,8 @@ void TopBar::setupUniqueBadgeTooltip() { if (!collectible || _localCollectible) { return; } - const auto parent = window(); _badgeTooltip = std::make_unique( - parent, + this, collectible, widget); const auto raw = _badgeTooltip.get(); @@ -1430,11 +1428,7 @@ void TopBar::setPatternEmojiId(std::optional patternEmojiId) { void TopBar::setLocalEmojiStatusId(EmojiStatusId emojiStatusId) { _localCollectible = emojiStatusId.collectible; - if (!emojiStatusId.collectible) { - _badgeContent = Badge::Content{ BadgeType::Premium, emojiStatusId }; - } else { - _badgeContent = BadgeContentForPeer(_peer); - } + _badgeContent = Badge::Content{ BadgeType::Premium, emojiStatusId }; updateCollectibleStatus(); } @@ -1960,7 +1954,7 @@ void TopBar::addTopBarEditButton( : st::infoTopBarBlackEdit))); _topBarButton->show(); _topBarButton->addClickHandler([=] { - controller->showSettings(::Settings::Information::Id()); + controller->showSettings(::Settings::InformationId()); }); widthValue() | rpl::on_next([=] { @@ -2001,10 +1995,11 @@ void TopBar::showTopBarMenu( } _peerMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); _peerMenu->popup(_actionMore - ? _actionMore->mapToGlobal( - QPoint( - _actionMore->width(), - _actionMore->height() + st::infoProfileTopBarActionMenuSkip)) + ? Ui::PopupMenu::ConstrainToParentScreen( + _peerMenu, + _actionMore->mapToGlobal(QPoint( + _actionMore->width() + _peerMenu->st().shadow.extend.right(), + _actionMore->height() + st::infoProfileTopBarActionMenuSkip))) : QCursor::pos()); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index 9eb64d256a..8bbd273f3e 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -8,8 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/settings/info_settings_widget.h" #include "info/info_memento.h" -#include "settings/settings_main.h" -#include "settings/settings_information.h" +#include "settings/sections/settings_main.h" +#include "settings/sections/settings_information.h" +#include "settings/settings_common_session.h" #include "ui/ui_utility.h" // AyuGram includes @@ -206,8 +207,8 @@ const Ui::RoundRect *Widget::bottomSkipRounding() const { } rpl::producer Widget::desiredShadowVisibility() const { - return (_type == ::Settings::Main::Id() - || _type == ::Settings::Information::Id() + return (_type == ::Settings::MainId() + || _type == ::Settings::InformationId() || _type == ::Settings::AyuMain::Id()) ? ContentWidget::desiredShadowVisibility() : rpl::single(true); @@ -261,9 +262,13 @@ void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { void Widget::saveState(not_null memento) { memento->setScrollTop(scrollTopSave()); + auto sectionState = std::any(); + _inner->sectionSaveState(sectionState); + memento->setSectionState(std::move(sectionState)); } void Widget::restoreState(not_null memento) { + _inner->sectionRestoreState(memento->sectionState()); scrollTopRestore(memento->scrollTop()); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index cab614b274..5070a7f2a5 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "info/info_flexible_scroll.h" +#include + namespace Settings { class AbstractSection; } // namespace Settings @@ -48,8 +50,16 @@ public: ~Memento(); + void setSectionState(std::any state) { + _sectionState = std::move(state); + } + [[nodiscard]] const std::any §ionState() const { + return _sectionState; + } + private: Type _type = Type(); + std::any _sectionState; }; diff --git a/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp b/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp index 88d1972c32..39eba79817 100644 --- a/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp +++ b/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp @@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/tooltip.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/window_session_controller.h" #include "styles/style_info.h" #include "styles/style_widgets.h" diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 306c04531c..ab04d9b938 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_credits.h" #include "api/api_statistics.h" +#include "boxes/peers/replace_boost_box.h" // GenerateGiftUniqueUserpicCallback #include "boxes/peer_list_controllers.h" #include "boxes/peer_list_widgets.h" #include "info/channel_statistics/earn/earn_icons.h" @@ -886,8 +887,9 @@ void CreditsRow::init() { ? name : _entry.postsSearch ? tr::lng_credits_box_history_entry_posts_search(tr::now) + : (_entry.giftUpgraded && _entry.uniqueGift && !isSpecial) + ? u"%1 #%2"_q.arg(_entry.uniqueGift->title).arg(Lang::FormatCountDecimal(_entry.uniqueGift->number)) : ((!_entry.subscriptionUntil.isNull() && !isSpecial) - || (_entry.giftUpgraded && !isSpecial) || (_entry.giftResale && !isSpecial) || _entry.title.isEmpty()) ? name @@ -922,6 +924,8 @@ void CreditsRow::init() { ? tr::lng_credits_box_history_entry_fragment(tr::now) : (_entry.gift && isSpecial) ? tr::lng_credits_box_history_entry_anonymous(tr::now) + : _entry.giftUpgraded + ? tr::lng_credits_box_history_entry_gift_upgrade(tr::now) : (_name == name) ? Ui::GenerateEntryName(_entry).text : name; @@ -938,7 +942,7 @@ void CreditsRow::init() { langDayOfMonthFull(_subscription.until.date()))); _description.setText(st::defaultTextStyle, _subscription.title); } - if (_entry.bareGiftStickerId) { + if (_entry.bareGiftStickerId && !_entry.giftUpgraded) { _description.setMarkedText( st::defaultTextStyle, Ui::Text::SingleCustomEmoji( @@ -1001,7 +1005,12 @@ void CreditsRow::init() { } } if (!_paintUserpicCallback) { - _paintUserpicCallback = (isSpecial || _entry.postsSearch) + _paintUserpicCallback = _entry.giftUpgraded + ? GenerateGiftUniqueUserpicCallback( + _session, + _entry.uniqueGift, + _context.repaint) + : (isSpecial || _entry.postsSearch) ? Ui::GenerateCreditsPaintUserpicCallback(_entry) : PeerListRow::generatePaintUserpicCallback(false); } diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index 08fbe4ef5e..93b61df858 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -545,6 +545,21 @@ void InnerWidget::enableBackButton() { void InnerWidget::showFinished() { _showFinished.fire({}); + + const auto window = _controller->parentController(); + window->checkHighlightControl(u"my-profile/posts"_q, _albumsWrap); + if (window->highlightControlId() == u"my-profile/posts/add-album"_q) { + window->setHighlightControlId(QString()); + if (_albumsWrap) { + ::Settings::HighlightWidget(_albumsWrap); + } + _controller->uiShow()->show(Box( + NewAlbumBox, + _controller, + _peer, + StoryId(), + [=](Data::StoryAlbum album) { albumAdded(album); })); + } } void InnerWidget::finalizeTop() { diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_color_circle_button.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_color_circle_button.cpp index 97ce58af6b..58bc3f8e24 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_color_circle_button.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_color_circle_button.cpp @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/userpic/info_userpic_color_circle_button.h" -#include "settings/settings_chat.h" // Settings::PaintRoundColorButton. +#include "settings/sections/settings_chat.h" // Settings::PaintRoundColorButton. #include "ui/painter.h" namespace UserpicBuilder { diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_menu_item.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_menu_item.cpp index 8dfaf73f42..7630dd3412 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_menu_item.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_menu_item.cpp @@ -207,7 +207,10 @@ void AddEmojiBuilderAction( state->manager.setDocuments(std::move(documents)); }, item->lifetime()); - menu->addAction(std::move(item)); + const auto action = menu->addAction(std::move(item)); + action->setProperty( + "highlight-control-id", + u"profile-photo/use-emoji"_q); } } // namespace UserpicBuilder diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 1e192c1477..74e7be9224 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -57,7 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "payments/payments_checkout_process.h" #include "payments/payments_non_panel_process.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "storage/storage_account.h" #include "storage/storage_domain.h" #include "ui/basic_click_handlers.h" @@ -1963,7 +1963,7 @@ void WebViewInstance::botSendPreparedMessage( if (!checked) { return; } - state->send = nullptr; + [[maybe_unused]] const auto ongoing = base::take(state->send); send({ strong }, options); }; state->send({}); diff --git a/Telegram/SourceFiles/intro/intro_code_input.cpp b/Telegram/SourceFiles/intro/intro_code_input.cpp index 4ceabf805e..97186f3504 100644 --- a/Telegram/SourceFiles/intro/intro_code_input.cpp +++ b/Telegram/SourceFiles/intro/intro_code_input.cpp @@ -69,6 +69,7 @@ public: protected: void paintEvent(QPaintEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; private: Shaker _shaker; @@ -157,9 +158,15 @@ void CodeDigit::paintEvent(QPaintEvent *e) { p.drawText(rect(), QString::number(_viewDigit), style::al_center); } +void CodeDigit::mouseReleaseEvent(QMouseEvent *e) { + QGuiApplication::inputMethod()->show(); + Ui::AbstractButton::mouseReleaseEvent(e); +} + CodeInput::CodeInput(QWidget *parent) : Ui::RpWidget(parent) { setFocusPolicy(Qt::StrongFocus); + setAttribute(Qt::WA_InputMethodEnabled); } QString CodeInput::accessibilityName() { @@ -260,19 +267,7 @@ void CodeInput::keyPressEvent(QKeyEvent *e) { _currentIndex = Circular(_currentIndex - 1, _digits.size()); unfocusAll(_currentIndex); } else if (key >= Qt::Key_0 && key <= Qt::Key_9) { - const auto index = int(key - Qt::Key_0); - _digits[_currentIndex]->setDigit(index); - _currentIndex = Circular(_currentIndex + 1, _digits.size()); - if (!_currentIndex) { - const auto result = collectDigits(); - if (result.size() == _digitsCountMax) { - _codeCollected.fire_copy(result); - _currentIndex = _digits.size() - 1; - } else { - findEmptyAndPerform([&](int i) { _currentIndex = i; }); - } - } - unfocusAll(_currentIndex); + processDigit(int(key - Qt::Key_0)); } else if (key == Qt::Key_Delete) { _digits[_currentIndex]->setDigit(kDigitNone); } else if (key == Qt::Key_Backspace) { @@ -296,6 +291,10 @@ void CodeInput::keyPressEvent(QKeyEvent *e) { } } +void CodeInput::keyReleaseEvent(QKeyEvent *e) { + QGuiApplication::inputMethod()->show(); +} + void CodeInput::contextMenuEvent(QContextMenuEvent *e) { if (_menu) { return; @@ -350,4 +349,40 @@ void CodeInput::findEmptyAndPerform(const Fn &callback) { } } +QVariant CodeInput::inputMethodQuery(Qt::InputMethodQuery query) const { + switch (query) { + case Qt::ImEnabled: + return true; + case Qt::ImHints: + return int(Qt::ImhDigitsOnly | Qt::ImhNoPredictiveText); + default: + return RpWidget::inputMethodQuery(query); + } +} + +void CodeInput::inputMethodEvent(QInputMethodEvent *e) { + const auto text = e->commitString(); + for (const auto &ch : text) { + if (ch.isDigit()) { + processDigit(ch.digitValue()); + } + } + e->accept(); +} + +void CodeInput::processDigit(int digit) { + _digits[_currentIndex]->setDigit(digit); + _currentIndex = Circular(_currentIndex + 1, _digits.size()); + if (!_currentIndex) { + const auto result = collectDigits(); + if (result.size() == _digitsCountMax) { + _codeCollected.fire_copy(result); + _currentIndex = _digits.size() - 1; + } else { + findEmptyAndPerform([&](int i) { _currentIndex = i; }); + } + } + unfocusAll(_currentIndex); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/intro/intro_code_input.h b/Telegram/SourceFiles/intro/intro_code_input.h index 9f9652401a..31fa74348c 100644 --- a/Telegram/SourceFiles/intro/intro_code_input.h +++ b/Telegram/SourceFiles/intro/intro_code_input.h @@ -39,7 +39,10 @@ protected: void focusOutEvent(QFocusEvent *e) override; void paintEvent(QPaintEvent *e) override; void keyPressEvent(QKeyEvent *e) override; + void keyReleaseEvent(QKeyEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; + QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; + void inputMethodEvent(QInputMethodEvent *e) override; private: [[nodiscard]] QString collectDigits() const; @@ -47,6 +50,7 @@ private: void insertCodeAndSubmit(const QString &code); void unfocusAll(int except); void findEmptyAndPerform(const Fn &callback); + void processDigit(int digit); int _digitsCountMax = 0; std::vector> _digits; diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index caa0cadb31..c37d2e2b95 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -619,7 +619,7 @@ void Controller::createWindow() { updateTitleGeometry(width); }, _subtitle->lifetime()); - window->setGeometry(_delegate->ivGeometry()); + window->setGeometry(_delegate->ivGeometry(window)); window->setMinimumSize({ st::windowMinWidth, st::windowMinHeight }); window->geometryValue( diff --git a/Telegram/SourceFiles/iv/iv_delegate.h b/Telegram/SourceFiles/iv/iv_delegate.h index d722d19f07..6e3a28cb8f 100644 --- a/Telegram/SourceFiles/iv/iv_delegate.h +++ b/Telegram/SourceFiles/iv/iv_delegate.h @@ -15,8 +15,7 @@ namespace Iv { class Delegate { public: - virtual void ivSetLastSourceWindow(not_null window) = 0; - [[nodiscard]] virtual QRect ivGeometry() const = 0; + [[nodiscard]] virtual QRect ivGeometry(not_null window) const = 0; virtual void ivSaveGeometry(not_null window) = 0; [[nodiscard]] virtual int ivZoom() const = 0; diff --git a/Telegram/SourceFiles/iv/iv_delegate_impl.cpp b/Telegram/SourceFiles/iv/iv_delegate_impl.cpp index e97de87bf8..f09418c836 100644 --- a/Telegram/SourceFiles/iv/iv_delegate_impl.cpp +++ b/Telegram/SourceFiles/iv/iv_delegate_impl.cpp @@ -45,26 +45,16 @@ namespace { } // namespace -void DelegateImpl::ivSetLastSourceWindow(not_null window) { - _lastSourceWindow = window; -} - -QRect DelegateImpl::ivGeometry() const { - const auto found = _lastSourceWindow - ? Core::App().findWindow(_lastSourceWindow) - : nullptr; - +QRect DelegateImpl::ivGeometry(not_null window) const { const auto saved = Core::App().settings().ivPosition(); const auto adjusted = Core::AdjustToScale(saved, u"IV"_q); const auto initial = DefaultPosition(); - auto result = initial.rect(); - if (const auto window = found ? found : Core::App().activeWindow()) { - result = window->widget()->countInitialGeometry( - adjusted, - initial, - { st::ivWidthMin, st::ivHeightMin }); - } - return result; + return Window::CountInitialGeometry( + window, + adjusted, + initial, + { st::ivWidthMin, st::ivHeightMin }, + u"IV"_q); } void DelegateImpl::ivSaveGeometry(not_null window) { @@ -100,7 +90,8 @@ void DelegateImpl::ivSaveGeometry(not_null window) { realPosition = Window::PositionWithScreen( realPosition, window, - { st::ivWidthMin, st::ivHeightMin }); + { st::ivWidthMin, st::ivHeightMin }, + u"IV"_q); if (realPosition.w >= st::ivWidthMin && realPosition.h >= st::ivHeightMin && realPosition != savedPosition) { diff --git a/Telegram/SourceFiles/iv/iv_delegate_impl.h b/Telegram/SourceFiles/iv/iv_delegate_impl.h index 8217d94383..e9767c5cfd 100644 --- a/Telegram/SourceFiles/iv/iv_delegate_impl.h +++ b/Telegram/SourceFiles/iv/iv_delegate_impl.h @@ -15,17 +15,13 @@ class DelegateImpl final : public Delegate { public: DelegateImpl() = default; - void ivSetLastSourceWindow(not_null window) override; - [[nodiscard]] QRect ivGeometry() const override; + [[nodiscard]] QRect ivGeometry(not_null window) const override; void ivSaveGeometry(not_null window) override; [[nodiscard]] int ivZoom() const override; [[nodiscard]] rpl::producer ivZoomValue() const override; void ivSetZoom(int value) override; -private: - QPointer _lastSourceWindow; - }; } // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index caab6362be..607ab87037 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -829,7 +829,6 @@ void Instance::show( not_null controller, not_null data, QString hash) { - _delegate->ivSetLastSourceWindow(controller->widget()); show(controller->uiShow(), data, hash); } diff --git a/Telegram/SourceFiles/lang/lang_keys.cpp b/Telegram/SourceFiles/lang/lang_keys.cpp index aa65ed81d7..8df01ea7e9 100644 --- a/Telegram/SourceFiles/lang/lang_keys.cpp +++ b/Telegram/SourceFiles/lang/lang_keys.cpp @@ -110,6 +110,20 @@ QString langDayOfMonthFull(const QDate &date) { }); } +QString langDayOfMonthShort(const QDate &date) { + auto day = date.day(); + return langDateMaybeWithYear(date, [&](int month, int year) { + return QLocale().toString(date, QLocale::ShortFormat); + }, [day](int month, int year) { + return tr::lng_month_day( + tr::now, + lt_month, + MonthSmall(month)(tr::now), + lt_day, + QString::number(day)); + }); +} + QString langMonthOfYear(int month, int year) { return (month > 0 && month <= 12) ? tr::lng_month_year( diff --git a/Telegram/SourceFiles/lang/lang_keys.h b/Telegram/SourceFiles/lang/lang_keys.h index f49f4fd32c..0a2a9b9f1a 100644 --- a/Telegram/SourceFiles/lang/lang_keys.h +++ b/Telegram/SourceFiles/lang/lang_keys.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL [[nodiscard]] QString langDayOfMonth(const QDate &date); [[nodiscard]] QString langDayOfMonthFull(const QDate &date); +[[nodiscard]] QString langDayOfMonthShort(const QDate &date); [[nodiscard]] QString langMonthOfYear(int month, int year); [[nodiscard]] QString langMonth(const QDate &date); [[nodiscard]] QString langMonthOfYearFull(int month, int year); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 469cb33e50..5828b19437 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -485,6 +485,35 @@ std::vector AppConfig::getIntArray( }); } +std::vector> AppConfig::getIntIntArray( + const QString &key, + std::vector> &&fallback) const { + return getValue(key, [&](const MTPJSONValue &value) { + return value.match([&](const MTPDjsonArray &data) { + auto result = std::vector>(); + result.reserve(data.vvalue().v.size()); + for (const auto &entry : data.vvalue().v) { + if (entry.type() != mtpc_jsonArray) { + return std::move(fallback); + } + const auto &list = entry.c_jsonArray().vvalue().v; + auto &last = result.emplace_back(); + last.reserve(list.size()); + for (const auto &inner : list) { + if (inner.type() != mtpc_jsonNumber) { + return std::move(fallback); + } + last.push_back( + int(base::SafeRound(inner.c_jsonNumber().vvalue().v))); + } + } + return result; + }, [&](const auto &data) { + return std::move(fallback); + }); + }); +} + std::vector AppConfig::getInt64Array( const QString &key, std::vector &&fallback) const { @@ -644,4 +673,15 @@ auto AppConfig::groupCallColorings() const -> std::vector { return _groupCallColorings; } +std::vector> AppConfig::craftAttributePermilles() const { + return get>>( + u"stargifts_craft_attribute_permilles"_q, + { + { 90 }, + { 80, 200 }, + { 70, 190, 460 }, + { 60, 180, 450, 1000 }, + }); +} + } // namespace Main diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 5e9cc97659..e1cc2f22be 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -47,6 +47,8 @@ public: return getStringMap(key, std::move(fallback)); } else if constexpr (std::is_same_v>) { return getIntArray(key, std::move(fallback)); + } else if constexpr (std::is_same_v>>) { + return getIntIntArray(key, std::move(fallback)); } else if constexpr (std::is_same_v>) { return getInt64Array(key, std::move(fallback)); } else if constexpr (std::is_same_v) { @@ -137,6 +139,8 @@ public: using StarsColoring = Calls::Group::Ui::StarsColoring; [[nodiscard]] std::vector groupCallColorings() const; + [[nodiscard]] std::vector> craftAttributePermilles() const; + void refresh(bool force = false); private: @@ -165,6 +169,9 @@ private: [[nodiscard]] std::vector getIntArray( const QString &key, std::vector &&fallback) const; + [[nodiscard]] std::vector> getIntIntArray( + const QString &key, + std::vector> &&fallback) const; [[nodiscard]] std::vector getInt64Array( const QString &key, std::vector &&fallback) const; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index dab2511cb7..dc1d3271e1 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" #include "data/components/top_peers.h" +#include "settings/settings_faq_suggestions.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_user.h" @@ -199,6 +200,7 @@ Session::Session( } })) , _passkeys(std::make_unique(this)) +, _faqSuggestions(std::make_unique(this)) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _fastButtonsBots(std::make_unique(this)) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index c6813fa2b4..085c74c1ff 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -45,6 +45,10 @@ class PromoSuggestions; class Passkeys; } // namespace Data +namespace Settings { +class FaqSuggestions; +} // namespace Settings + namespace HistoryView::Reactions { class CachedIconFactory; } // namespace HistoryView::Reactions @@ -205,6 +209,9 @@ public: [[nodiscard]] Data::Passkeys &passkeys() const { return *_passkeys; } + [[nodiscard]] Settings::FaqSuggestions &faqSuggestions() const { + return *_faqSuggestions; + } [[nodiscard]] auto cachedReactionIconFactory() const -> HistoryView::Reactions::CachedIconFactory & { return *_cachedReactionIconFactory; @@ -310,6 +317,7 @@ private: const std::unique_ptr _credits; const std::unique_ptr _promoSuggestions; const std::unique_ptr _passkeys; + const std::unique_ptr _faqSuggestions; using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; const std::unique_ptr _cachedReactionIconFactory; diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 0be24c19c7..9169e20702 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -67,7 +67,7 @@ QByteArray SessionSettings::serialize() const { } size += sizeof(qint32); // _setupEmailState size += sizeof(qint32) // _moderateCommonGroups size - + (_moderateCommonGroups.size() * sizeof(quint64)); + + (_moderateCommonGroups.size() * sizeof(qint32)); auto result = QByteArray(); result.reserve(size); @@ -150,8 +150,8 @@ QByteArray SessionSettings::serialize() const { } stream << qint32(static_cast(_setupEmailState)); stream << qint32(_moderateCommonGroups.size()); - for (const auto &peerId : _moderateCommonGroups) { - stream << SerializePeerId(peerId); + for (const auto &filterId : _moderateCommonGroups) { + stream << qint32(filterId); } } @@ -225,7 +225,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { base::flat_set ratedTranscriptions; std::vector unreviewed; qint32 setupEmailState = 0; - std::vector moderateCommonGroups; + std::vector moderateCommonGroups; stream >> versionTag; if (versionTag == kVersionTag) { @@ -647,15 +647,15 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { stream >> count; if (stream.status() == QDataStream::Ok) { for (auto i = 0; i != count; ++i) { - quint64 peerId; - stream >> peerId; + qint32 filterId; + stream >> filterId; if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for SessionSettings::addFromSerialized()" "with moderateCommonGroups")); return; } - moderateCommonGroups.emplace_back(DeserializePeerId(peerId)); + moderateCommonGroups.emplace_back(filterId); } } } diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index 16e4f07ab2..56982c7902 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -176,10 +176,10 @@ public: void setSetupEmailState(Data::SetupEmailState state); [[nodiscard]] Data::SetupEmailState setupEmailState() const; - void setModerateCommonGroups(std::vector groups) { + void setModerateCommonGroups(std::vector groups) { _moderateCommonGroups = std::move(groups); } - [[nodiscard]] const std::vector &moderateCommonGroups() const { + [[nodiscard]] const std::vector &moderateCommonGroups() const { return _moderateCommonGroups; } @@ -229,7 +229,7 @@ private: Data::SetupEmailState _setupEmailState; - std::vector _moderateCommonGroups; + std::vector _moderateCommonGroups; }; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 8ef6cc6668..26cfaebb7d 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -70,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/player/media_player_dropdown.h" #include "media/player/media_player_instance.h" #include "base/qthelp_regex.h" +#include "base/options.h" #include "mtproto/mtproto_dc_options.h" #include "core/update_checker.h" #include "core/shortcuts.h" @@ -87,7 +88,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_app_config.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "support/support_helper.h" #include "storage/storage_user_photos.h" #include "styles/style_dialogs.h" @@ -109,8 +110,16 @@ void ClearBotStartToken(PeerData *peer) { } } +base::options::toggle ForceComposeSearchOneColumn({ + .id = kForceComposeSearchOneColumn, + .name = "Force embedded search in chats", + .description = "Force in one-column mode the embedded search in chats.", +}); + } // namespace +const char kForceComposeSearchOneColumn[] = "force-compose-search-one-column"; + enum StackItemType { HistoryStackItem, SectionStackItem, @@ -788,7 +797,8 @@ void MainWidget::searchMessages( return; } auto tags = Data::SearchTagsFromQuery(query); - if (_dialogs) { + if (_dialogs + && (!ForceComposeSearchOneColumn.value() || !isOneColumn())) { auto state = Dialogs::SearchState{ .inChat = ((tags.empty() || inChat.sublist()) ? inChat @@ -822,11 +832,11 @@ void MainWidget::searchMessages( const auto account = not_null(&session().account()); if (const auto window = Core::App().windowFor(account)) { if (const auto controller = window->sessionController()) { + controller->widget()->activate(); controller->content()->searchMessages( query, inChat, searchFrom); - controller->widget()->activate(); } } } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index fc9281f9fe..198d2fffee 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -92,6 +92,8 @@ namespace Core { class Changelogs; } // namespace Core +extern const char kForceComposeSearchOneColumn[]; + class MainWidget final : public Ui::RpWidget , private Media::Player::FloatDelegate { diff --git a/Telegram/SourceFiles/media/player/media_player.style b/Telegram/SourceFiles/media/player/media_player.style index 4a64006d29..ff549c5d77 100644 --- a/Telegram/SourceFiles/media/player/media_player.style +++ b/Telegram/SourceFiles/media/player/media_player.style @@ -64,6 +64,7 @@ MediaSpeedButton { rippleRadius: pixels; menu: MediaSpeedMenu; menuAlign: align; + menuPosition: point; } mediaPlayerButton: MediaPlayerButton { @@ -228,6 +229,7 @@ mediaPlayerSpeedButton: MediaSpeedButton { rippleRadius: 4px; menu: mediaPlayerSpeedMenu; menuAlign: align(topright); + menuPosition: point(-2px, -1px); } mediaPlayerVolumeIcon0: icon { diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp index cc563ab53b..113bffdd33 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp @@ -495,11 +495,13 @@ WithDropdownController::WithDropdownController( not_null menuParent, const style::DropdownMenu &menuSt, Qt::Alignment menuAlign, + QPoint menuPosition, Fn menuOverCallback) : _button(button) , _menuParent(menuParent) , _menuSt(menuSt) , _menuAlign(menuAlign) +, _menuPosition(menuPosition) , _menuOverCallback(std::move(menuOverCallback)) { button->events( ) | rpl::filter([=](not_null e) { @@ -534,8 +536,8 @@ void WithDropdownController::updateDropdownGeometry() { const auto mwidth = _menu->width(); const auto mheight = _menu->height(); const auto padding = _menuSt.wrap.padding; - const auto x = st::mediaPlayerMenuPosition.x(); - const auto y = st::mediaPlayerMenuPosition.y(); + const auto x = _menuPosition.x(); + const auto y = _menuPosition.y(); const auto position = _menu->parentWidget()->mapFromGlobal( _button->mapToGlobal(QPoint()) ) + [&] { @@ -636,6 +638,7 @@ OrderController::OrderController( menuParent, st::mediaPlayerMenu, style::al_topright, + st::mediaPlayerMenuPosition, std::move(menuOverCallback)) , _button(button) , _appOrder(std::move(value)) @@ -724,6 +727,7 @@ SpeedController::SpeedController( menuParent, st.menu.dropdown, st.menuAlign, + st.menuPosition, std::move(menuOverCallback)) , _st(st) , _lookup(std::move(value)) diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.h b/Telegram/SourceFiles/media/player/media_player_dropdown.h index 1a0e0bb08c..f75a235b51 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.h +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.h @@ -73,6 +73,7 @@ public: not_null menuParent, const style::DropdownMenu &menuSt, Qt::Alignment menuAlign, + QPoint menuPosition, Fn menuOverCallback); virtual ~WithDropdownController() = default; @@ -95,6 +96,7 @@ private: const not_null _menuParent; const style::DropdownMenu &_menuSt; const Qt::Alignment _menuAlign = Qt::AlignTop | Qt::AlignRight; + const QPoint _menuPosition; const Fn _menuOverCallback; base::unique_qptr _menu; rpl::variable _menuToggled; diff --git a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp index 14b52d36c3..209ef77766 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/compose/compose_show.h" #include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_view.h" +#include "media/view/media_view_open_common.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/labels.h" #include "ui/click_handler.h" @@ -30,7 +31,9 @@ CaptionFullView::CaptionFullView(not_null controller) object_ptr(_scroll.get(), st::storiesCaptionFull), st::mediaviewCaptionPadding + _controller->repostCaptionPadding()))) , _text(_wrap->entity()) { - _text->setMarkedText(controller->captionText(), Core::TextContext({ + using namespace Media::View; + const auto text = StripQuoteEntities(controller->captionText()); + _text->setMarkedText(text, Core::TextContext({ .session = &controller->uiShow()->session(), })); diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 17a1d55eff..f1113de0eb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -249,19 +249,23 @@ struct MadePrivacyBadge { const auto index = data.fullIndex + 1; const auto count = data.fullCount; return count - ? QString::fromUtf8(" \xE2\x80\xA2 %1/%2").arg(index).arg(count) + ? QString::fromUtf8(" %3 %1/%2") + .arg(index) + .arg(count) + .arg(Ui::kQBullet) : QString(); } [[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) { auto result = ComposeTimestamp(data.date, now); if (data.edited) { - result.text.append( - QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now)); + result.text.append(' ' + + Ui::kQBullet + + ' ' + + tr::lng_edited(tr::now)); } if (data.fromPeer || !data.repostFrom.isEmpty()) { - result.text = QString::fromUtf8("\xE2\x80\xA2 ") - + result.text; + result.text = Ui::kQBullet + ' ' + result.text; } return result; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index c40e80be6e..ac07131c63 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -129,9 +129,9 @@ constexpr auto kLoadViewsPages = 2; const QString &date, not_null repost) { return date + (repost->repostModified() - ? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now)) + ? (' ' + Ui::kQBullet + ' ' + tr::lng_edited(tr::now)) : !repost->caption().empty() - ? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_commented(tr::now)) + ? (' ' + Ui::kQBullet + ' ' + tr::lng_commented(tr::now)) : QString()); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp index d0ead0ccf7..89b8d533ae 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -17,7 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "ui/controls/feature_list.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 42ade93681..8615199591 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -215,6 +215,7 @@ mediaviewTextStyle: defaultTextStyle; mediaviewTextLeft: 14px; mediaviewTextSkip: 10px; +mediaviewTextSkipHalf: 5px; mediaviewHeaderTop: 47px; mediaviewTextTop: 26px; @@ -368,6 +369,7 @@ mediaviewSpeedButton: MediaSpeedButton(mediaPlayerSpeedButton) { rippleRadius: 16px; menu: mediaviewSpeedMenu; menuAlign: align(bottomright); + menuPosition: point(-2px, 31px); } themePreviewSize: size(903px, 584px); @@ -492,10 +494,6 @@ storiesLike: IconButton(storiesAttach) { icon: icon {{ "chat/input_like", storiesComposeGrayIcon }}; iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }}; } -storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; -storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; -storiesRecordRound: icon {{ "chat/input_video", storiesComposeGrayIcon }}; -storiesRecordRoundOver: icon {{ "chat/input_video", storiesComposeGrayIcon }}; storiesEditStars: IconButton(storiesAttach) { icon: icon{{ "chat/input_paid", storiesComposeGrayIcon }}; iconOver: icon{{ "chat/input_paid", storiesComposeGrayIcon }}; @@ -727,10 +725,6 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { rippleAreaSize: 40px; rippleAreaPosition: point(1px, 1px); } - record: storiesRecordVoice; - recordOver: storiesRecordVoiceOver; - round: storiesRecordRound; - roundOver: storiesRecordRoundOver; sendDisabledFg: storiesComposeGrayText; } attach: storiesAttach; diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.cpp b/Telegram/SourceFiles/media/view/media_view_open_common.cpp index 346a87ef30..de40d8e307 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.cpp +++ b/Telegram/SourceFiles/media/view/media_view_open_common.cpp @@ -25,4 +25,17 @@ TimeId ExtractVideoTimestamp(not_null item) { return 0; } +TextWithEntities StripQuoteEntities(TextWithEntities text) { + for (auto i = text.entities.begin(); i != text.entities.end();) { + if (i->type() == EntityType::Blockquote) { + i = text.entities.erase(i); + continue; + } else if (i->type() == EntityType::Pre) { + *i = EntityInText(EntityType::Code, i->offset(), i->length()); + } + ++i; + } + return text; +} + } // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index c65b52838d..8655926354 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -170,4 +170,6 @@ private: [[nodiscard]] TimeId ExtractVideoTimestamp(not_null item); +[[nodiscard]] TextWithEntities StripQuoteEntities(TextWithEntities text); + } // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 2f9aa48dc7..e1da206d12 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -105,6 +105,34 @@ float roundedCorner() { }; } +[[nodiscard]] QRectF StoryCropTextureRect( + QSizeF imageSize, + QSizeF targetSize) { + if (imageSize.isEmpty() || targetSize.isEmpty()) { + return QRectF(0., 0., 1., 1.); + } + const auto targetAspect = targetSize.width() / targetSize.height(); + const auto imageAspect = imageSize.width() / imageSize.height(); + if (imageAspect > targetAspect) { + const auto cropW = imageSize.height() * targetAspect; + const auto offset = (imageSize.width() - cropW) / 2.; + return QRectF( + offset / imageSize.width(), + 0., + cropW / imageSize.width(), + 1.); + } else if (imageAspect < targetAspect) { + const auto cropH = imageSize.width() / targetAspect; + const auto offset = (imageSize.height() - cropH) / 2.; + return QRectF( + 0., + offset / imageSize.height(), + 1., + cropH / imageSize.height()); + } + return QRectF(0., 0., 1., 1.); +} + } // namespace OverlayWidget::RendererGL::RendererGL(not_null owner) @@ -433,7 +461,10 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( program->setUniformValue("f_texture", GLint(nv12 ? 2 : 3)); toggleBlending(geometry.roundRadius > 0.); - paintTransformedContent(program, geometry, false); + const auto textureRect = _owner->_stories + ? StoryCropTextureRect(QSizeF(yuv->size), geometry.rect.size()) + : QRectF(0., 0., 1., 1.); + paintTransformedContent(program, geometry, false, textureRect); if (_owner->_recognitionResult.success && !_owner->_recognitionResult.items.empty()) { @@ -517,7 +548,14 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( toggleBlending((geometry.roundRadius > 0.) || (semiTransparent && !fillTransparentBackground)); - paintTransformedContent(&*program, geometry, fillTransparentBackground); + const auto textureRect = _owner->_stories + ? StoryCropTextureRect(QSizeF(image.size()), geometry.rect.size()) + : QRectF(0., 0., 1., 1.); + paintTransformedContent( + &*program, + geometry, + fillTransparentBackground, + textureRect); if (_owner->_recognitionResult.success && !_owner->_recognitionResult.items.empty() @@ -533,7 +571,8 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( void OverlayWidget::RendererGL::paintTransformedContent( not_null program, ContentGeometry geometry, - bool fillTransparentBackground) { + bool fillTransparentBackground, + QRectF textureRect) { const auto rect = scaleRect( transformRect(geometry.rect), geometry.scale); @@ -553,18 +592,22 @@ void OverlayWidget::RendererGL::paintTransformedContent( const auto topright = rotated(rect.right(), rect.top()); const auto bottomright = rotated(rect.right(), rect.bottom()); const auto bottomleft = rotated(rect.left(), rect.bottom()); + const auto texLeft = float(textureRect.x()); + const auto texRight = float(textureRect.x() + textureRect.width()); + const auto texTop = 1.f - float(textureRect.y()); + const auto texBottom = 1.f - float(textureRect.y() + textureRect.height()); const GLfloat coords[] = { topleft[0], topleft[1], - 0.f, 1.f, + texLeft, texTop, topright[0], topright[1], - 1.f, 1.f, + texRight, texTop, bottomright[0], bottomright[1], - 1.f, 0.f, + texRight, texBottom, bottomleft[0], bottomleft[1], - 0.f, 0.f, + texLeft, texBottom, }; _contentBuffer->bind(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index bd387bf12d..e2e973e514 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -50,7 +50,8 @@ private: void paintTransformedContent( not_null program, ContentGeometry geometry, - bool fillTransparentBackground); + bool fillTransparentBackground, + QRectF textureRect = QRectF(0., 0., 1., 1.)); void paintRadialLoading( QRect inner, bool radial, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp index 3d2e668431..d45becf852 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp @@ -15,6 +15,33 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_media_view.h" namespace Media::View { +namespace { + +[[nodiscard]] QRectF StoryCropRect(QSizeF imageSize, QSizeF targetSize) { + if (imageSize.isEmpty() || targetSize.isEmpty()) { + return QRectF(); + } + const auto targetAspect = targetSize.width() / targetSize.height(); + const auto imageAspect = imageSize.width() / imageSize.height(); + if (imageAspect > targetAspect) { + const auto cropW = imageSize.height() * targetAspect; + return QRectF( + (imageSize.width() - cropW) / 2., + 0., + cropW, + imageSize.height()); + } else if (imageAspect < targetAspect) { + const auto cropH = imageSize.width() / targetAspect; + return QRectF( + 0., + (imageSize.height() - cropH) / 2., + imageSize.width(), + cropH); + } + return QRectF(); +} + +} // namespace OverlayWidget::RendererSW::RendererSW(not_null owner) : _owner(owner) @@ -100,7 +127,11 @@ void OverlayWidget::RendererSW::paintTransformedVideoFrame( if (!rect.intersects(_clipOuter)) { return; } - paintTransformedImage(_owner->videoFrame(), rect, rotation); + const auto image = _owner->videoFrame(); + const auto sourceRect = _owner->_stories + ? StoryCropRect(QSizeF(image.size()), geometry.rect.size()) + : QRectF(); + paintTransformedImage(image, rect, rotation, sourceRect); paintControlsFade(rect, geometry); } @@ -120,7 +151,10 @@ void OverlayWidget::RendererSW::paintTransformedStaticContent( _p->fillRect(rect, _transparentBrush); } if (!image.isNull()) { - paintTransformedImage(image, rect, rotation); + const auto sourceRect = _owner->_stories + ? StoryCropRect(QSizeF(image.size()), geometry.rect.size()) + : QRectF(); + paintTransformedImage(image, rect, rotation, sourceRect); } paintControlsFade(rect, geometry); } @@ -192,14 +226,19 @@ void OverlayWidget::RendererSW::paintControlsFade( void OverlayWidget::RendererSW::paintTransformedImage( const QImage &image, QRect rect, - int rotation) { + int rotation, + const QRectF &sourceRect) { PainterHighQualityEnabler hq(*_p); if (UsePainterRotation(rotation)) { if (rotation) { _p->save(); _p->rotate(rotation); } - _p->drawImage(RotatedRect(rect, rotation), image); + if (sourceRect.isValid()) { + _p->drawImage(QRectF(RotatedRect(rect, rotation)), image, sourceRect); + } else { + _p->drawImage(RotatedRect(rect, rotation), image); + } if (rotation) { _p->restore(); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h index e3ab9b8986..f8b2d89f1d 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h @@ -33,7 +33,8 @@ private: void paintTransformedImage( const QImage &image, QRect rect, - int rotation); + int rotation, + const QRectF &sourceRect = QRectF()); void paintControlsFade(QRect content, const ContentGeometry &geometry); void paintRadialLoading( QRect inner, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index dd357f692a..b98d056c8c 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -962,13 +962,12 @@ void OverlayWidget::initNormalGeometry() { const auto saved = Core::App().settings().mediaViewPosition(); const auto adjusted = Core::AdjustToScale(saved, u"Viewer"_q); const auto initial = DefaultPosition(); - _normalGeometry = initial.rect(); - if (const auto active = Core::App().activeWindow()) { - _normalGeometry = active->widget()->countInitialGeometry( - adjusted, - initial, - { st::mediaviewMinWidth, st::mediaviewMinHeight }); - } + _normalGeometry = Window::CountInitialGeometry( + _window, + adjusted, + initial, + { st::mediaviewMinWidth, st::mediaviewMinHeight }, + u"Viewer"_q); } void OverlayWidget::savePosition() { @@ -1007,7 +1006,8 @@ void OverlayWidget::savePosition() { realPosition = Window::PositionWithScreen( realPosition, _window, - { st::mediaviewMinWidth, st::mediaviewMinHeight }); + { st::mediaviewMinWidth, st::mediaviewMinHeight }, + u"Viewer"_q); if (realPosition.w >= st::mediaviewMinWidth && realPosition.h >= st::mediaviewMinHeight && realPosition != savedPosition) { @@ -1029,12 +1029,24 @@ void OverlayWidget::updateGeometry(bool inMove) { if (_fullscreen) { updateGeometryToScreen(inMove); } else if (_windowed && _normalGeometryInited) { - DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4") - .arg(_normalGeometry.x()) - .arg(_normalGeometry.y()) - .arg(_normalGeometry.width()) - .arg(_normalGeometry.height())); - _window->setGeometry(_normalGeometry); + const auto setGeometry = [=](QRect geometry) { + DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4") + .arg(geometry.x()) + .arg(geometry.y()) + .arg(geometry.width()) + .arg(geometry.height())); + _window->setGeometry(geometry); + }; + const auto geometry = _normalGeometry; + setGeometry(geometry); + if constexpr (Platform::IsMac()) { + // Either macOS or Qt immediately overwrite the geometry for + // some time (perhaps until the window is really on screen), + // try to set again later in event loop + InvokeQueued(_window, [=] { + setGeometry(geometry); + }); + } } if constexpr (!Platform::IsMac()) { if (_fullscreen) { @@ -1504,13 +1516,26 @@ void OverlayWidget::updateControls() { height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height); + const auto separatorWidth = st::mediaviewFont->width(Ui::kQBullet); + _separatorNav = QRect( + st::mediaviewTextLeft + + _nameNav.width() + + st::mediaviewTextSkipHalf, + height() - st::mediaviewTextTop, + separatorWidth, + st::mediaviewFont->height); _dateNav = QRect( - st::mediaviewTextLeft + _nameNav.width() + st::mediaviewTextSkip, + st::mediaviewTextLeft + + _nameNav.width() + + st::mediaviewTextSkipHalf + + separatorWidth + + st::mediaviewTextSkipHalf, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height); } else { _nameNav = QRect(); + _separatorNav = QRect(); _dateNav = QRect( st::mediaviewTextLeft, height() - st::mediaviewTextTop, @@ -1990,6 +2015,7 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { : QRect()) + _headerNav + _nameNav + + _separatorNav + _dateNav + _captionRect.marginsAdded(st::mediaviewCaptionPadding) + _groupThumbsRect @@ -3431,7 +3457,7 @@ void OverlayWidget::refreshFromLabel() { void OverlayWidget::refreshCaption() { _caption = Ui::Text::String(); - const auto caption = [&] { + const auto caption = StripQuoteEntities([&] { if (_stories) { return _stories->captionText(); } else if (_message) { @@ -3449,7 +3475,7 @@ void OverlayWidget::refreshCaption() { return _message->translatedText(); } return TextWithEntities(); - }(); + }()); if (caption.text.isEmpty()) { return; } @@ -5626,6 +5652,18 @@ void OverlayWidget::paintFooterContent( } } + // separator + if (_separatorNav.isValid()) { + const auto separator = _separatorNav.translated(shift); + if (separator.intersects(clip)) { + p.setOpacity(controlOpacity(0.) * opacity); + p.drawText( + separator.left(), + separator.top() + st::mediaviewFont->ascent, + Ui::kQBullet); + } + } + // date if (date.intersects(clip)) { float64 o = overLevel(Over::Date); @@ -5640,7 +5678,7 @@ void OverlayWidget::paintFooterContent( } QRect OverlayWidget::footerGeometry() const { - return _headerNav.united(_nameNav).united(_dateNav); + return _headerNav.united(_nameNav).united(_separatorNav).united(_dateNav); } void OverlayWidget::paintCaptionContent( @@ -6551,6 +6589,7 @@ void OverlayWidget::handleMouseRelease( } if (_recognitionResult.success + && !_dragging && !_recognitionResult.items.empty() && _showRecognitionResults && button == Qt::LeftButton) { @@ -6560,6 +6599,9 @@ void OverlayWidget::handleMouseRelease( QString(), { tr::lng_text_copied(tr::now) }, 1000); + _over = _down = Over::None; + _pressed = false; + _dragging = 0; return; } } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index f52d368d9f..0ffe6deb62 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -590,7 +590,7 @@ private: QRect _leftNav, _leftNavOver, _leftNavIcon; QRect _rightNav, _rightNavOver, _rightNavIcon; - QRect _headerNav, _nameNav, _dateNav; + QRect _headerNav, _nameNav, _dateNav, _separatorNav; QRect _rotateNav, _rotateNavOver, _rotateNavIcon; QRect _shareNav, _shareNavOver, _shareNavIcon; QRect _recognizeNav, _recognizeNavOver, _recognizeNavIcon; diff --git a/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp index b099efb4cd..805f31003a 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp @@ -179,7 +179,6 @@ void Close::paintEvent(QPaintEvent *e) { (height() - st::mediaSponsoredCloseDiameter) / 2, st::mediaSponsoredCloseDiameter, st::mediaSponsoredCloseDiameter); - p.setFont(st::mediaSponsoredCloseFont); _countdown.paint( p, inner.x() + (inner.width() - _countdown.countWidth()) / 2, @@ -487,6 +486,7 @@ void PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) { int PlaybackSponsored::Message::resizeGetHeight(int newWidth) { const auto &padding = st::mediaSponsoredPadding; const auto userpic = st::mediaSponsoredThumb; + _left = padding.left() + (_photo ? (userpic + padding.left()) : 0); const auto innerWidth = newWidth - _left - _close->width(); const auto titleWidth = innerWidth - _about->width() - padding.right(); _titleHeight = _title.countHeight(titleWidth); @@ -495,7 +495,6 @@ int PlaybackSponsored::Message::resizeGetHeight(int newWidth) { const auto use = std::max(_titleHeight + _textHeight, userpic); const auto height = padding.top() + use + padding.bottom(); - _left = padding.left() + (_photo ? (userpic + padding.left()) : 0); _top = padding.top() + (use - _titleHeight - _textHeight) / 2; _about->move( diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 4aee09d8d7..77254b6e45 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -50,7 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "main/main_session.h" #include "apiwrap.h" -#include "settings/settings_premium.h" +#include "settings/sections/settings_premium.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "styles/style_chat.h" diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index a249507623..77968bd224 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -86,7 +86,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#31774388 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; +user#31774388 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -102,7 +102,7 @@ chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; channel#1c32b11c flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?RecentStory color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; -channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; +channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true monoforum:flags.10?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; channelFull#e4e0b29d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab = ChatFull; @@ -188,7 +188,7 @@ messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_am messageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction; messageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long transaction_id:string boost_peer:Peer giveaway_msg_id:int = MessageAction; messageActionStarGift#ea2c31d3 flags:# name_hidden:flags.0?true saved:flags.2?true converted:flags.3?true upgraded:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true prepaid_upgrade:flags.13?true upgrade_separate:flags.16?true auction_acquired:flags.17?true gift:StarGift message:flags.1?TextWithEntities convert_stars:flags.4?long upgrade_msg_id:flags.5?int upgrade_stars:flags.8?long from_id:flags.11?Peer peer:flags.12?Peer saved_id:flags.12?long prepaid_upgrade_hash:flags.14?string gift_msg_id:flags.15?int to_id:flags.18?Peer gift_num:flags.19?int = MessageAction; -messageActionStarGiftUnique#95728543 flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true prepaid_upgrade:flags.11?true assigned:flags.13?true from_offer:flags.14?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long resale_amount:flags.8?StarsAmount can_transfer_at:flags.9?int can_resell_at:flags.10?int drop_original_details_stars:flags.12?long = MessageAction; +messageActionStarGiftUnique#e6c31522 flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true prepaid_upgrade:flags.11?true assigned:flags.13?true from_offer:flags.14?true craft:flags.16?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long resale_amount:flags.8?StarsAmount can_transfer_at:flags.9?int can_resell_at:flags.10?int drop_original_details_stars:flags.12?long can_craft_at:flags.15?int = MessageAction; messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction; messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction; messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector = MessageAction; @@ -201,6 +201,8 @@ messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currenc messageActionSuggestBirthday#2c8f2a25 birthday:Birthday = MessageAction; messageActionStarGiftPurchaseOffer#774278d4 flags:# accepted:flags.0?true declined:flags.1?true gift:StarGift price:StarsAmount expires_at:int = MessageAction; messageActionStarGiftPurchaseOfferDeclined#73ada76b flags:# expired:flags.0?true gift:StarGift price:StarsAmount = MessageAction; +messageActionNewCreatorPending#b07ed085 new_creator_id:long = MessageAction; +messageActionChangeCreator#e188503b new_creator_id:long = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -454,6 +456,7 @@ updateDeleteGroupCallMessages#3e85e92c call:InputGroupCall messages:Vector updateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Update; updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update; updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update; +updateStarGiftCraftFail#ac072444 = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -677,24 +680,24 @@ botCommand#c27ac8c7 command:string description:string = BotCommand; botInfo#4d8a0299 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string app_settings:flags.8?BotAppSettings verifier_settings:flags.9?BotVerifierSettings = BotInfo; -keyboardButton#a2fa4880 text:string = KeyboardButton; -keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; -keyboardButtonCallback#35bbdb6b flags:# requires_password:flags.0?true text:string data:bytes = KeyboardButton; -keyboardButtonRequestPhone#b16a6c29 text:string = KeyboardButton; -keyboardButtonRequestGeoLocation#fc796b3f text:string = KeyboardButton; -keyboardButtonSwitchInline#93b9fbb5 flags:# same_peer:flags.0?true text:string query:string peer_types:flags.1?Vector = KeyboardButton; -keyboardButtonGame#50f41ccf text:string = KeyboardButton; -keyboardButtonBuy#afd93fbb text:string = KeyboardButton; -keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; -inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; -keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton; -inputKeyboardButtonUserProfile#e988037b text:string user_id:InputUser = KeyboardButton; -keyboardButtonUserProfile#308660c1 text:string user_id:long = KeyboardButton; -keyboardButtonWebView#13767230 text:string url:string = KeyboardButton; -keyboardButtonSimpleWebView#a0c0505c text:string url:string = KeyboardButton; -keyboardButtonRequestPeer#53d7bfd8 text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; -inputKeyboardButtonRequestPeer#c9662d05 flags:# name_requested:flags.0?true username_requested:flags.1?true photo_requested:flags.2?true text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; -keyboardButtonCopy#75d2698e text:string copy_text:string = KeyboardButton; +keyboardButton#7d170cff flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton; +keyboardButtonUrl#d80c25ec flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton; +keyboardButtonCallback#e62bc960 flags:# requires_password:flags.0?true style:flags.10?KeyboardButtonStyle text:string data:bytes = KeyboardButton; +keyboardButtonRequestPhone#417efd8f flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton; +keyboardButtonRequestGeoLocation#aa40f94d flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton; +keyboardButtonSwitchInline#991399fc flags:# same_peer:flags.0?true style:flags.10?KeyboardButtonStyle text:string query:string peer_types:flags.1?Vector = KeyboardButton; +keyboardButtonGame#89c590f9 flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton; +keyboardButtonBuy#3fa53905 flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton; +keyboardButtonUrlAuth#f51006f9 flags:# style:flags.10?KeyboardButtonStyle text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; +inputKeyboardButtonUrlAuth#68013e72 flags:# request_write_access:flags.0?true style:flags.10?KeyboardButtonStyle text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; +keyboardButtonRequestPoll#7a11d782 flags:# style:flags.10?KeyboardButtonStyle quiz:flags.0?Bool text:string = KeyboardButton; +inputKeyboardButtonUserProfile#7d5e07c7 flags:# style:flags.10?KeyboardButtonStyle text:string user_id:InputUser = KeyboardButton; +keyboardButtonUserProfile#c0fd5d09 flags:# style:flags.10?KeyboardButtonStyle text:string user_id:long = KeyboardButton; +keyboardButtonWebView#e846b1a0 flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton; +keyboardButtonSimpleWebView#e15c4370 flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton; +keyboardButtonRequestPeer#5b0f15f5 flags:# style:flags.10?KeyboardButtonStyle text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; +inputKeyboardButtonRequestPeer#2b78156 flags:# name_requested:flags.0?true username_requested:flags.1?true photo_requested:flags.2?true style:flags.10?KeyboardButtonStyle text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; +keyboardButtonCopy#bcc4af10 flags:# style:flags.10?KeyboardButtonStyle text:string copy_text:string = KeyboardButton; keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; @@ -1261,8 +1264,8 @@ folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; -urlAuthResultRequest#92d33a0e flags:# request_write_access:flags.0?true bot:User domain:string = UrlAuthResult; -urlAuthResultAccepted#8f8c0e4e url:string = UrlAuthResult; +urlAuthResultRequest#32fabf1a flags:# request_write_access:flags.0?true request_phone_number:flags.1?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string = UrlAuthResult; +urlAuthResultAccepted#623a8fa0 flags:# url:flags.0?string = UrlAuthResult; urlAuthResultDefault#a9d6db1f = UrlAuthResult; channelLocationEmpty#bfb5ad8b = ChannelLocation; @@ -1919,7 +1922,7 @@ starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; starGift#313a9547 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true require_premium:flags.7?true limited_per_user:flags.8?true peer_color_available:flags.10?true auction:flags.11?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer per_user_total:flags.8?int per_user_remains:flags.8?int locked_until_date:flags.9?int auction_slug:flags.11?string gifts_per_round:flags.11?int auction_start_date:flags.11?int upgrade_variants:flags.12?int background:flags.13?StarGiftBackground = StarGift; -starGiftUnique#569d64c9 flags:# require_premium:flags.6?true resale_ton_only:flags.7?true theme_available:flags.9?true id:long gift_id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_amount:flags.4?Vector released_by:flags.5?Peer value_amount:flags.8?long value_currency:flags.8?string value_usd_amount:flags.8?long theme_peer:flags.10?Peer peer_color:flags.11?PeerColor host_id:flags.12?Peer offer_min_stars:flags.13?int = StarGift; +starGiftUnique#85f0a9cd flags:# require_premium:flags.6?true resale_ton_only:flags.7?true theme_available:flags.9?true burned:flags.14?true crafted:flags.15?true id:long gift_id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_amount:flags.4?Vector released_by:flags.5?Peer value_amount:flags.8?long value_currency:flags.8?string value_usd_amount:flags.8?long theme_peer:flags.10?Peer peer_color:flags.11?PeerColor host_id:flags.12?Peer offer_min_stars:flags.13?int craft_chance_permille:flags.16?int = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#2ed82995 hash:int gifts:Vector chats:Vector users:Vector = payments.StarGifts; @@ -1954,9 +1957,9 @@ botVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true botVerification#f93cd45c bot_id:long icon:long description:string = BotVerification; -starGiftAttributeModel#39d99013 name:string document:Document rarity_permille:int = StarGiftAttribute; -starGiftAttributePattern#13acff19 name:string document:Document rarity_permille:int = StarGiftAttribute; -starGiftAttributeBackdrop#d93d859c name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity_permille:int = StarGiftAttribute; +starGiftAttributeModel#565251e2 flags:# crafted:flags.0?true name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributePattern#4e7085ea name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributeBackdrop#9f2504e4 name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity:StarGiftAttributeRarity = StarGiftAttribute; starGiftAttributeOriginalDetails#e0bff26c flags:# sender_id:flags.0?Peer recipient_id:Peer date:int message:flags.1?TextWithEntities = StarGiftAttribute; payments.starGiftUpgradePreview#3de1dfed sample_attributes:Vector prices:Vector next_prices:Vector = payments.StarGiftUpgradePreview; @@ -1968,7 +1971,7 @@ payments.uniqueStarGift#416c56e8 gift:StarGift chats:Vector users:Vector users:Vector = messages.WebPagePreview; -savedStarGift#ead6805e flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true upgrade_separate:flags.17?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector prepaid_upgrade_hash:flags.16?string drop_original_details_stars:flags.18?long gift_num:flags.19?int = SavedStarGift; +savedStarGift#41df43fc flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true upgrade_separate:flags.17?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector prepaid_upgrade_hash:flags.16?string drop_original_details_stars:flags.18?long gift_num:flags.19?int can_craft_at:flags.20?int = SavedStarGift; payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; @@ -2113,6 +2116,14 @@ messages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:l messages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo; messages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector plays_left:flags.0?int = messages.EmojiGameInfo; +starGiftAttributeRarity#36437737 permille:int = StarGiftAttributeRarity; +starGiftAttributeRarityUncommon#dbce6389 = StarGiftAttributeRarity; +starGiftAttributeRarityRare#f08d516b = StarGiftAttributeRarity; +starGiftAttributeRarityEpic#78fbf3a8 = StarGiftAttributeRarity; +starGiftAttributeRarityLegendary#cef7e7a8 = StarGiftAttributeRarity; + +keyboardButtonStyle#4fdd3430 flags:# bg_primary:flags.0?true bg_danger:flags.1?true bg_success:flags.2?true icon:flags.3?long = KeyboardButtonStyle; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2423,7 +2434,7 @@ messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector = Vector; messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; -messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; +messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true share_phone_number:flags.3?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; messages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages; messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector = messages.Messages; @@ -2659,6 +2670,7 @@ channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Upda channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; +channels.getFutureCreatorAfterLeave#a00918af channel:InputChannel = User; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2739,7 +2751,7 @@ payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password: payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector = Bool; payments.canPurchaseStore#4fdc5ea7 purpose:InputStorePaymentPurpose = Bool; -payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector offset:string limit:int = payments.ResaleStarGifts; +payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true for_craft:flags.4?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector offset:string limit:int = payments.ResaleStarGifts; payments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates; payments.createStarGiftCollection#1f4a0e87 peer:InputPeer title:string stargift:Vector = StarGiftCollection; payments.updateStarGiftCollection#4fddbee7 flags:# peer:InputPeer collection_id:int title:flags.0?string delete_stargift:flags.1?Vector add_stargift:flags.2?Vector order:flags.3?Vector = StarGiftCollection; @@ -2754,6 +2766,8 @@ payments.getStarGiftActiveAuctions#a5d0514d hash:long = payments.StarGiftActiveA payments.resolveStarGiftOffer#e9ce781c flags:# decline:flags.0?true offer_msg_id:int = Updates; payments.sendStarGiftOffer#8fb86b41 flags:# peer:InputPeer slug:string price:StarsAmount duration:int random_id:long allow_paid_stars:flags.0?long = Updates; payments.getStarGiftUpgradeAttributes#6d038b58 gift_id:long = payments.StarGiftUpgradeAttributes; +payments.getCraftStarGifts#fd05dd00 gift_id:long offset:string limit:int = payments.SavedStarGifts; +payments.craftStarGift#b0f9684f stargift:Vector = Updates; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2889,4 +2903,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 221 +// LAYER 222 diff --git a/Telegram/SourceFiles/passport/ui/passport_details_row.cpp b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp index 1239ea01cb..df899454b9 100644 --- a/Telegram/SourceFiles/passport/ui/passport_details_row.cpp +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp @@ -919,7 +919,10 @@ int GenderRow::resizeInner(int left, int top, int width) { top += st::passportDetailsField.textMargins.top(); top -= st::defaultCheckbox.textPosition.y(); _male->moveToLeft(left, top); - left += _male->widthNoMargins() + st::passportDetailsGenderSkip; + left += _male->checkRect().width() + + st::defaultCheckbox.style.font->width( + tr::lng_passport_gender_male(tr::now)) + + st::passportDetailsGenderSkip; _female->moveToLeft(left, top); return st::semiboldFont->height; } diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index e704bd3389..fa517bf9c2 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -46,64 +46,77 @@ void ProcessCreditsPayment( QPointer fireworks, std::shared_ptr form, Fn maybeReturnToBot) { - const auto done = [=](Settings::SmallBalanceResult result) { + const auto hasBadResult = [=](Settings::SmallBalanceResult result) { if (result == Settings::SmallBalanceResult::Blocked) { if (const auto onstack = maybeReturnToBot) { onstack(CheckoutResult::Failed); } - return; + return true; } else if (result == Settings::SmallBalanceResult::Cancelled) { if (const auto onstack = maybeReturnToBot) { onstack(CheckoutResult::Cancelled); } + return true; + } + return false; + }; + const auto done = [=](Settings::SmallBalanceResult result) { + if (hasBadResult(result)) { return; - } else if (form->starGiftForm - || IsPremiumForStarsInvoice(form->id)) { - const auto done = [=](std::optional error) { - const auto onstack = maybeReturnToBot; - if (error) { - if (*error == u"STARGIFT_USAGE_LIMITED"_q) { - if (form->starGiftLimitedCount) { - show->showToast({ - .title = tr::lng_gift_sold_out_title( - tr::now), - .text = tr::lng_gift_sold_out_text( - tr::now, - lt_count_decimal, - form->starGiftLimitedCount, - tr::rich), - }); - } else { - show->showToast( - tr::lng_gift_sold_out_title(tr::now)); - } - } else if (*error == u"STARGIFT_USER_USAGE_LIMITED"_q) { + } + const auto done = [=](std::optional error) { + const auto onstack = maybeReturnToBot; + if (error) { + if (*error == u"STARGIFT_USAGE_LIMITED"_q) { + if (form->starGiftLimitedCount) { show->showToast({ - .text = tr::lng_gift_sent_finished( + .title = tr::lng_gift_sold_out_title( + tr::now), + .text = tr::lng_gift_sold_out_text( tr::now, - lt_count, - std::max(form->starGiftPerUserLimit, 1), + lt_count_decimal, + form->starGiftLimitedCount, tr::rich), }); } else { - show->showToast(*error); + show->showToast( + tr::lng_gift_sold_out_title(tr::now)); } - if (onstack) { - onstack(CheckoutResult::Failed); - } - } else if (onstack) { - onstack(CheckoutResult::Paid); + } else if (*error == u"STARGIFT_USER_USAGE_LIMITED"_q) { + show->showToast({ + .text = tr::lng_gift_sent_finished( + tr::now, + lt_count, + std::max(form->starGiftPerUserLimit, 1), + tr::rich), + }); + } else { + show->showToast(*error); } - }; - Ui::SendStarsForm(&show->session(), form, done); - return; - } + if (onstack) { + onstack(CheckoutResult::Failed); + } + } else if (onstack) { + onstack(CheckoutResult::Paid); + } + }; + Ui::SendStarsForm(&show->session(), form, done); + }; + using namespace Settings; + auto source = Ui::SmallBalanceSourceFromForm(form); + if (form->starGiftForm || IsPremiumForStarsInvoice(form->id)) { + const auto credits = form->invoice.credits; + MaybeRequestBalanceIncrease(show, credits, source, done); + } else { const auto unsuccessful = std::make_shared(true); const auto box = show->show(Box( Ui::SendCreditsBox, form, - [=] { + [=](Settings::SmallBalanceResult result) { *unsuccessful = false; + if (hasBadResult(result)) { + return; + } if (const auto widget = fireworks.data()) { Ui::StartFireworks(widget); } @@ -120,15 +133,7 @@ void ProcessCreditsPayment( } }); }, box->lifetime()); - }; - using namespace Settings; - const auto starGift = std::get_if(&form->id.value); - auto source = !starGift - ? SmallBalanceSource(SmallBalanceBot{ .botId = form->botId }) - : SmallBalanceSource(SmallBalanceStarGift{ - .recipientId = starGift->recipient->id, - }); - MaybeRequestBalanceIncrease(show, form->invoice.credits, source, done); + } } void ProcessCreditsReceipt( diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 60ffa0bb19..ea51876ac6 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -31,7 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { namespace { -class AwayMessage final : public BusinessSection { +class AwayMessage final : public Section { public: AwayMessage( QWidget *parent, @@ -190,7 +190,7 @@ void AddAwayScheduleSelector( AwayMessage::AwayMessage( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) { +: Section(parent, controller) { setupContent(controller); } diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp index 34a7dcb70d..76c02c8756 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -119,7 +119,7 @@ private: }; -class ChatIntro final : public BusinessSection { +class ChatIntro final : public Section { public: ChatIntro( QWidget *parent, @@ -497,7 +497,7 @@ void StickerPanel::create(const Descriptor &descriptor) { ChatIntro::ChatIntro( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) { +: Section(parent, controller) { setupContent(controller); } diff --git a/Telegram/SourceFiles/settings/business/settings_chat_links.cpp b/Telegram/SourceFiles/settings/business/settings_chat_links.cpp index 8df87f2dec..784b643bda 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_links.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_links.cpp @@ -56,7 +56,7 @@ constexpr auto kChangesDebounceTimeout = crl::time(1000); using ChatLinkData = Api::ChatLink; -class ChatLinks final : public BusinessSection { +class ChatLinks final : public Section { public: ChatLinks( QWidget *parent, @@ -698,7 +698,7 @@ void LinksController::rowPaintIcon( ChatLinks::ChatLinks( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) +: Section(parent, controller) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index c5c93f670b..2f9fccf849 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -53,7 +53,7 @@ struct BotState { return Data::ChatbotsPermission::ViewMessages; } -class Chatbots final : public BusinessSection { +class Chatbots final : public Section { public: Chatbots( QWidget *parent, @@ -393,7 +393,7 @@ Main::Session &PreviewController::session() const { Chatbots::Chatbots( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) +: Section(parent, controller) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(); } diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 7f6f575172..1f96ca1a1f 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -35,7 +35,7 @@ namespace { constexpr auto kDefaultNoActivityDays = 7; -class Greeting : public BusinessSection { +class Greeting : public Section { public: Greeting( QWidget *parent, @@ -66,7 +66,7 @@ private: Greeting::Greeting( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) +: Section(parent, controller) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp index e3b332163d..57098491a0 100644 --- a/Telegram/SourceFiles/settings/business/settings_location.cpp +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -37,7 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { namespace { -class Location : public BusinessSection { +class Location : public Section { public: Location( QWidget *parent, @@ -84,7 +84,7 @@ private: Location::Location( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) +: Section(parent, controller) , _config(ResolveBusinessMapsConfig(&controller->session())) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index 562b8f6700..3632174528 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -33,7 +33,7 @@ namespace { constexpr auto kShortcutLimit = 32; -class QuickReplies : public BusinessSection { +class QuickReplies : public Section { public: QuickReplies( QWidget *parent, @@ -52,7 +52,7 @@ private: QuickReplies::QuickReplies( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) { +: Section(parent, controller) { setupContent(controller); } diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h index bda2026f2e..a5528dfa69 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h @@ -23,33 +23,6 @@ class SessionController; namespace Settings { -template -class BusinessSection : public Section { -public: - BusinessSection( - QWidget *parent, - not_null controller) - : Section(parent) - , _controller(controller) { - } - - [[nodiscard]] not_null controller() const { - return _controller; - } - [[nodiscard]] rpl::producer<> showFinishes() const { - return _showFinished.events(); - } - -private: - void showFinished() override { - _showFinished.fire({}); - } - - const not_null _controller; - rpl::event_stream<> _showFinished; - -}; - struct BusinessChatsDescriptor { Data::BusinessChats current; Fn save; diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 420c8d9247..9af25687ed 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -330,7 +330,7 @@ ShortcutMessages::ShortcutMessages( not_null scroll, rpl::producer containerValue, BusinessShortcutId shortcutId) -: AbstractSection(parent) +: AbstractSection(parent, controller) , WindowListDelegate(controller) , _controller(controller) , _session(&controller->session()) diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 58c9d47864..a47391e90b 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -38,7 +38,7 @@ constexpr auto kDay = Data::WorkingInterval::kDay; constexpr auto kWeek = Data::WorkingInterval::kWeek; constexpr auto kInNextDayMax = Data::WorkingInterval::kInNextDayMax; -class WorkingHours : public BusinessSection { +class WorkingHours : public Section { public: WorkingHours( QWidget *parent, @@ -575,7 +575,7 @@ void AddWeekButton( WorkingHours::WorkingHours( QWidget *parent, not_null controller) -: BusinessSection(parent, controller) { +: Section(parent, controller) { setupContent(controller); } diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp index e20cc28883..b9917e268b 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp @@ -53,11 +53,11 @@ BottomButton CreateBottomDisableButton( Ui::AddSkip(content); - content->add(object_ptr